@webority/chat-react 0.22.0 → 0.23.0

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 CHANGED
@@ -194,11 +194,13 @@ Escape closes it, and the question box takes focus when it opens.
194
194
  | `resolveLink`, `linkComponent` | Reply links; see Replies below. |
195
195
  | `confirmAction`, `onViewTicket` | Support action cards; see `ActionCard`. |
196
196
  | `rating` | Offer ratings under stored replies. Default true. |
197
+ | `renderReply` | Draws each assistant reply your way; see Custom reply rendering below. |
197
198
 
198
199
  ### `<ChatThread />`, `<ChatMessage />`, `<ChatComposer />`
199
200
 
200
201
  The panel's parts, for a product that lays out its own assistant page. `ChatThread` takes
201
- `turns`, `empty`, `onRate`, `resolveLink`, `linkComponent`, `confirmAction` and `onViewTicket`.
202
+ `turns`, `empty`, `onRate`, `resolveLink`, `linkComponent`, `confirmAction`, `onViewTicket` and
203
+ `renderReply`.
202
204
  Each answer is a polite live region, marked busy while it streams. `ChatComposer` takes
203
205
  `onSubmit`, `busy`, `disabled`, `maxLength` and `inputRef`; Enter sends, Shift and Enter starts a
204
206
  new line.
@@ -231,3 +233,21 @@ A link shows only when its href has the form `scheme:kind/id` (for example `tick
231
233
  and `resolveLink(href)` returns an in-app route starting with `/`. Any other link, including every
232
234
  web address, shows its label as plain text. Pass the router's `Link` as `linkComponent` so a link
233
235
  navigates inside the portal.
236
+
237
+ ### Custom reply rendering
238
+
239
+ A product that draws replies its own way (charts, maths) passes `renderReply` to `ChatPanel`,
240
+ `ChatThread` or `ChatMessage`. It is called as `renderReply(turn, { streaming })` for each
241
+ assistant reply that has text, and what it returns replaces `ChatMarkdown`. `turn.answer` is the
242
+ text so far, and `streaming` is true while it is still arriving. The person's question, the
243
+ "thinking" state, errors, action cards, the rating and the layout stay the library's. With no
244
+ `renderReply`, replies render as above.
245
+
246
+ ```jsx
247
+ <ChatPanel
248
+ chat={chat}
249
+ open={open}
250
+ onClose={() => setOpen(false)}
251
+ renderReply={(turn, { streaming }) => <RichReply text={turn.answer} live={streaming} />}
252
+ />
253
+ ```
@@ -471,21 +471,21 @@ function re({ client: a, onOpen: s, onDeleted: c, pageSize: l = 20, labels: d })
471
471
  }
472
472
  //#endregion
473
473
  //#region src/components/ChatLauncher.jsx
474
- var ie = { zIndex: "var(--wui-z-fixed)" }, ae = {
475
- end: "end-0",
476
- start: "start-0"
474
+ var ie = {
475
+ zIndex: "var(--wui-z-fixed)",
476
+ bottom: 0
477
+ }, ae = {
478
+ end: { insetInlineEnd: 0 },
479
+ start: { insetInlineStart: 0 }
477
480
  };
478
481
  function oe({ onClick: e, open: t = !1, position: r = "end", icon: i = "sparkles", className: a, style: o, labels: s }) {
479
482
  let c = M(s);
480
483
  return /* @__PURE__ */ b("span", {
481
484
  "data-support-capture": "exclude",
482
- className: [
483
- "position-fixed bottom-0 m-3",
484
- ae[r],
485
- a
486
- ].filter(Boolean).join(" "),
485
+ className: ["position-fixed m-3", a].filter(Boolean).join(" "),
487
486
  style: {
488
487
  ...ie,
488
+ ...ae[r],
489
489
  ...o
490
490
  },
491
491
  children: /* @__PURE__ */ b(n, {
@@ -842,8 +842,8 @@ var pe = {
842
842
  whiteSpace: "pre-wrap",
843
843
  overflowWrap: "anywhere"
844
844
  };
845
- function Q({ turn: e, onRate: n, resolveLink: i, linkComponent: a, confirmAction: o, onViewTicket: s, labels: c }) {
846
- let l = M(c), u = e.streaming && !e.answer;
845
+ function Q({ turn: e, onRate: n, resolveLink: i, linkComponent: a, confirmAction: o, onViewTicket: s, renderReply: c, labels: l }) {
846
+ let u = M(l), f = e.streaming && !e.answer;
847
847
  return /* @__PURE__ */ x("li", {
848
848
  className: "d-flex flex-column gap-2",
849
849
  children: [e.question && /* @__PURE__ */ x("div", {
@@ -851,7 +851,7 @@ function Q({ turn: e, onRate: n, resolveLink: i, linkComponent: a, confirmAction
851
851
  style: pe,
852
852
  children: [/* @__PURE__ */ x("span", {
853
853
  className: "visually-hidden",
854
- children: [l.you, ": "]
854
+ children: [u.you, ": "]
855
855
  }), /* @__PURE__ */ b(r, {
856
856
  padding: "sm",
857
857
  variant: "muted",
@@ -864,25 +864,25 @@ function Q({ turn: e, onRate: n, resolveLink: i, linkComponent: a, confirmAction
864
864
  children: [
865
865
  /* @__PURE__ */ x("span", {
866
866
  className: "visually-hidden",
867
- children: [l.assistant, ": "]
867
+ children: [u.assistant, ": "]
868
868
  }),
869
869
  /* @__PURE__ */ b("div", {
870
870
  "aria-live": "polite",
871
871
  "aria-busy": e.streaming,
872
- children: u ? /* @__PURE__ */ x("span", {
872
+ children: f ? /* @__PURE__ */ x("span", {
873
873
  className: "d-inline-flex align-items-center gap-2 wui-text-sm text-body-secondary",
874
874
  children: [/* @__PURE__ */ b(d, {
875
875
  size: "sm",
876
- label: l.thinking
876
+ label: u.thinking
877
877
  }), /* @__PURE__ */ b("span", {
878
878
  "aria-hidden": "true",
879
- children: l.thinking
879
+ children: u.thinking
880
880
  })]
881
- }) : e.answer && /* @__PURE__ */ b(X, {
881
+ }) : e.answer && (c ? c(e, { streaming: !!e.streaming }) : /* @__PURE__ */ b(X, {
882
882
  text: e.answer,
883
883
  resolveLink: i,
884
884
  linkComponent: a
885
- })
885
+ }))
886
886
  }),
887
887
  e.error && /* @__PURE__ */ b(t, {
888
888
  size: "sm",
@@ -893,12 +893,12 @@ function Q({ turn: e, onRate: n, resolveLink: i, linkComponent: a, confirmAction
893
893
  action: e,
894
894
  confirmAction: o,
895
895
  onViewTicket: s,
896
- labels: c
896
+ labels: l
897
897
  }, e.token)),
898
898
  n && e.messageId && !e.streaming && /* @__PURE__ */ b(Z, {
899
899
  rating: e.rating,
900
900
  onRate: (t, r) => n(e.id, t, r),
901
- labels: c
901
+ labels: l
902
902
  })
903
903
  ]
904
904
  })]
@@ -906,23 +906,23 @@ function Q({ turn: e, onRate: n, resolveLink: i, linkComponent: a, confirmAction
906
906
  }
907
907
  //#endregion
908
908
  //#region src/components/ChatThread.jsx
909
- function $({ turns: e, empty: t, onRate: n, resolveLink: r, linkComponent: i, confirmAction: a, onViewTicket: s, labels: c }) {
910
- let l = M(c), u = _(null), d = e[e.length - 1];
909
+ function $({ turns: e, empty: t, onRate: n, resolveLink: r, linkComponent: i, confirmAction: a, onViewTicket: s, renderReply: c, labels: l }) {
910
+ let u = M(l), d = _(null), f = e[e.length - 1];
911
911
  return h(() => {
912
- u.current?.scrollIntoView({ block: "end" });
912
+ d.current?.scrollIntoView({ block: "end" });
913
913
  }, [
914
914
  e.length,
915
- d?.answer,
916
- d?.error,
917
- d?.actions
915
+ f?.answer,
916
+ f?.error,
917
+ f?.actions
918
918
  ]), e.length === 0 ? t ?? /* @__PURE__ */ b(o, {
919
919
  icon: "sparkles",
920
920
  size: "sm",
921
- title: l.emptyTitle,
922
- text: l.emptyText
921
+ title: u.emptyTitle,
922
+ text: u.emptyText
923
923
  }) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("ol", {
924
924
  className: "list-unstyled d-flex flex-column gap-3 mb-0",
925
- "aria-label": l.thread,
925
+ "aria-label": u.thread,
926
926
  children: e.map((e) => /* @__PURE__ */ b(Q, {
927
927
  turn: e,
928
928
  onRate: n,
@@ -930,18 +930,19 @@ function $({ turns: e, empty: t, onRate: n, resolveLink: r, linkComponent: i, co
930
930
  linkComponent: i,
931
931
  confirmAction: a,
932
932
  onViewTicket: s,
933
- labels: c
933
+ renderReply: c,
934
+ labels: l
934
935
  }, e.id))
935
- }), /* @__PURE__ */ b("div", { ref: u })] });
936
+ }), /* @__PURE__ */ b("div", { ref: d })] });
936
937
  }
937
938
  //#endregion
938
939
  //#region src/components/ChatPanel.jsx
939
- function me({ chat: n, open: r, onClose: i, title: a, titleIcon: o = "sparkles", headerActions: c, empty: u, disclaimer: f, maxLength: p = 4e3, allowanceThreshold: m = 10, width: g = "440px", resolveLink: v, linkComponent: y, confirmAction: S, onViewTicket: C, rating: w = !0, labels: T }) {
940
- let E = M(T), D = _(null), { refreshAllowance: O } = n;
940
+ function me({ chat: n, open: r, onClose: i, title: a, titleIcon: o = "sparkles", headerActions: c, empty: u, disclaimer: f, maxLength: p = 4e3, allowanceThreshold: m = 10, width: g = "440px", resolveLink: v, linkComponent: y, confirmAction: S, onViewTicket: C, rating: w = !0, renderReply: T, labels: E }) {
941
+ let D = M(E), O = _(null), { refreshAllowance: k } = n;
941
942
  h(() => {
942
- r && O().catch(() => {});
943
- }, [r, O]);
944
- let k = n.messagesLeftToday, A = a ?? E.panelTitle;
943
+ r && k().catch(() => {});
944
+ }, [r, k]);
945
+ let A = n.messagesLeftToday, j = a ?? D.panelTitle;
945
946
  return /* @__PURE__ */ b(l, {
946
947
  show: r,
947
948
  onHide: i,
@@ -957,20 +958,20 @@ function me({ chat: n, open: r, onClose: i, title: a, titleIcon: o = "sparkles",
957
958
  name: o,
958
959
  size: 18
959
960
  })
960
- }), A]
961
+ }), j]
961
962
  }), /* @__PURE__ */ x("div", {
962
963
  className: "d-flex align-items-center gap-1",
963
964
  children: [
964
965
  c,
965
966
  /* @__PURE__ */ b(e, {
966
967
  icon: "plus",
967
- label: E.newChat,
968
+ label: D.newChat,
968
969
  onClick: n.newConversation,
969
970
  disabled: n.turns.length === 0 && !n.loading
970
971
  }),
971
972
  /* @__PURE__ */ b(e, {
972
973
  icon: "x",
973
- label: E.close,
974
+ label: D.close,
974
975
  onClick: i
975
976
  })
976
977
  ]
@@ -979,30 +980,30 @@ function me({ chat: n, open: r, onClose: i, title: a, titleIcon: o = "sparkles",
979
980
  footer: /* @__PURE__ */ x("div", {
980
981
  className: "d-flex flex-column gap-2",
981
982
  children: [
982
- k !== null && k <= m && /* @__PURE__ */ b("p", {
983
+ A !== null && A <= m && /* @__PURE__ */ b("p", {
983
984
  className: "wui-text-xs text-body-secondary mb-0",
984
985
  role: "status",
985
- children: E.allowance(k)
986
+ children: D.allowance(A)
986
987
  }),
987
988
  /* @__PURE__ */ b(I, {
988
989
  onSubmit: n.submit,
989
990
  busy: n.streaming || n.loading,
990
- disabled: k === 0,
991
+ disabled: A === 0,
991
992
  maxLength: p,
992
- inputRef: D,
993
- labels: T
993
+ inputRef: O,
994
+ labels: E
994
995
  }),
995
996
  /* @__PURE__ */ b("p", {
996
997
  className: "wui-text-xs text-body-secondary mb-0",
997
- children: f ?? E.disclaimer
998
+ children: f ?? D.disclaimer
998
999
  })
999
1000
  ]
1000
1001
  }),
1001
- ariaLabel: typeof A == "string" ? A : E.panelTitle,
1002
- onEntered: () => D.current?.focus(),
1002
+ ariaLabel: typeof j == "string" ? j : D.panelTitle,
1003
+ onEntered: () => O.current?.focus(),
1003
1004
  children: n.loading ? /* @__PURE__ */ b("div", {
1004
1005
  className: "d-flex justify-content-center py-4",
1005
- children: /* @__PURE__ */ b(d, { label: E.loadingConversation })
1006
+ children: /* @__PURE__ */ b(d, { label: D.loadingConversation })
1006
1007
  }) : n.loadError ? /* @__PURE__ */ b(t, {
1007
1008
  variant: "danger",
1008
1009
  children: n.loadError.message
@@ -1014,7 +1015,8 @@ function me({ chat: n, open: r, onClose: i, title: a, titleIcon: o = "sparkles",
1014
1015
  linkComponent: y,
1015
1016
  confirmAction: S,
1016
1017
  onViewTicket: C,
1017
- labels: T
1018
+ renderReply: T,
1019
+ labels: E
1018
1020
  })
1019
1021
  });
1020
1022
  }
@@ -12,9 +12,12 @@
12
12
  * @param {(token: string) => Promise<{ id: string, number: string }>} [props.confirmAction] Shows
13
13
  * support action cards; see `ActionCard`.
14
14
  * @param {(ticket: { id: string, number: string }) => void} [props.onViewTicket]
15
+ * @param {(turn: import('../useChat.js').ChatTurn, state: { streaming: boolean }) => import('react').ReactNode} [props.renderReply]
16
+ * Draws the assistant's answer in place of `ChatMarkdown` (charts, maths). The question, action
17
+ * cards, rating and layout stay the library's.
15
18
  * @param {Partial<typeof import('../labels.js').DEFAULT_CHAT_LABELS>} [props.labels]
16
19
  */
17
- export default function ChatMessage({ turn, onRate, resolveLink, linkComponent, confirmAction, onViewTicket, labels: labelOverrides }: {
20
+ export default function ChatMessage({ turn, onRate, resolveLink, linkComponent, confirmAction, onViewTicket, renderReply, labels: labelOverrides, }: {
18
21
  turn: import('../useChat.js').ChatTurn;
19
22
  onRate?: (turnId: string, rating: "Up" | "Down", note?: string) => Promise<unknown>;
20
23
  resolveLink?: (href: string) => string | null | undefined;
@@ -27,5 +30,8 @@ export default function ChatMessage({ turn, onRate, resolveLink, linkComponent,
27
30
  id: string;
28
31
  number: string;
29
32
  }) => void;
33
+ renderReply?: (turn: import('../useChat.js').ChatTurn, state: {
34
+ streaming: boolean;
35
+ }) => import('react').ReactNode;
30
36
  labels?: Partial<typeof import('../labels.js').DEFAULT_CHAT_LABELS>;
31
37
  }): import("react").JSX.Element;
@@ -23,9 +23,11 @@
23
23
  * support action cards, e.g. `supportClient.confirmAction`.
24
24
  * @param {(ticket: { id: string, number: string }) => void} [props.onViewTicket]
25
25
  * @param {boolean} [props.rating=true] Offer thumbs under stored replies.
26
+ * @param {(turn: import('../useChat.js').ChatTurn, state: { streaming: boolean }) => import('react').ReactNode} [props.renderReply]
27
+ * Draws each assistant answer instead of `ChatMarkdown`; see `ChatMessage`.
26
28
  * @param {Partial<typeof import('../labels.js').DEFAULT_CHAT_LABELS>} [props.labels]
27
29
  */
28
- export default function ChatPanel({ chat, open, onClose, title, titleIcon, headerActions, empty, disclaimer, maxLength, allowanceThreshold, width, resolveLink, linkComponent, confirmAction, onViewTicket, rating, labels: labelOverrides, }: {
30
+ export default function ChatPanel({ chat, open, onClose, title, titleIcon, headerActions, empty, disclaimer, maxLength, allowanceThreshold, width, resolveLink, linkComponent, confirmAction, onViewTicket, rating, renderReply, labels: labelOverrides, }: {
29
31
  chat: ReturnType<typeof import('../useChat.js').useChat>;
30
32
  open: boolean;
31
33
  onClose: () => void;
@@ -48,5 +50,8 @@ export default function ChatPanel({ chat, open, onClose, title, titleIcon, heade
48
50
  number: string;
49
51
  }) => void;
50
52
  rating?: boolean;
53
+ renderReply?: (turn: import('../useChat.js').ChatTurn, state: {
54
+ streaming: boolean;
55
+ }) => import('react').ReactNode;
51
56
  labels?: Partial<typeof import('../labels.js').DEFAULT_CHAT_LABELS>;
52
57
  }): import("react").JSX.Element;
@@ -10,9 +10,11 @@
10
10
  * @param {import('react').ElementType} [props.linkComponent]
11
11
  * @param {(token: string) => Promise<{ id: string, number: string }>} [props.confirmAction]
12
12
  * @param {(ticket: { id: string, number: string }) => void} [props.onViewTicket]
13
+ * @param {(turn: import('../useChat.js').ChatTurn, state: { streaming: boolean }) => import('react').ReactNode} [props.renderReply]
14
+ * Draws each assistant answer instead of `ChatMarkdown`; see `ChatMessage`.
13
15
  * @param {Partial<typeof import('../labels.js').DEFAULT_CHAT_LABELS>} [props.labels]
14
16
  */
15
- export default function ChatThread({ turns, empty, onRate, resolveLink, linkComponent, confirmAction, onViewTicket, labels: labelOverrides, }: {
17
+ export default function ChatThread({ turns, empty, onRate, resolveLink, linkComponent, confirmAction, onViewTicket, renderReply, labels: labelOverrides, }: {
16
18
  turns: import('../useChat.js').ChatTurn[];
17
19
  empty?: import('react').ReactNode;
18
20
  onRate?: (turnId: string, rating: "Up" | "Down", note?: string) => Promise<unknown>;
@@ -26,5 +28,8 @@ export default function ChatThread({ turns, empty, onRate, resolveLink, linkComp
26
28
  id: string;
27
29
  number: string;
28
30
  }) => void;
31
+ renderReply?: (turn: import('../useChat.js').ChatTurn, state: {
32
+ streaming: boolean;
33
+ }) => import('react').ReactNode;
29
34
  labels?: Partial<typeof import('../labels.js').DEFAULT_CHAT_LABELS>;
30
35
  }): string | number | bigint | boolean | import("react").JSX.Element | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | Iterable<import("react").ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/chat-react",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Webority AI assistant for React portals: a streaming chat client, a store-agnostic chat hook, and the launcher, panel, thread, rating and support action card, built on @webority/ui-react.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "react": "^19.0.0",
33
33
  "react-bootstrap": "^2.10.0",
34
34
  "react-dom": "^19.0.0",
35
- "@webority/ui-react": "0.22.0"
35
+ "@webority/ui-react": "0.23.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/dom": "10.4.2",
@@ -50,7 +50,7 @@
50
50
  "typescript": "^7.0.2",
51
51
  "vite": "^8.0.12",
52
52
  "vitest": "5.0.1",
53
- "@webority/ui-react": "0.22.0"
53
+ "@webority/ui-react": "0.23.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "vite build && tsc -p tsconfig.json",