@clubmed/trident-ui 2.1.0-beta.3 → 2.1.0-beta.4

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.
Files changed (107) hide show
  1. package/README.md +1 -0
  2. package/chunks/ChatMessage.js +282 -265
  3. package/chunks/ChatMessage.js.map +1 -1
  4. package/examples/chat-body-demo.js +4 -2
  5. package/examples/chat-body-demo.js.map +1 -1
  6. package/examples/chat-button-demo.js +2 -1
  7. package/examples/chat-button-demo.js.map +1 -1
  8. package/examples/chat-button-offset-demo.js +2 -1
  9. package/examples/chat-button-offset-demo.js.map +1 -1
  10. package/examples/chat-header-demo.js +2 -1
  11. package/examples/chat-header-demo.js.map +1 -1
  12. package/examples/chat-input-demo.js +2 -1
  13. package/examples/chat-input-demo.js.map +1 -1
  14. package/examples/chat-input-streaming-demo.d.ts +1 -0
  15. package/examples/chat-input-streaming-demo.js +30 -0
  16. package/examples/chat-input-streaming-demo.js.map +1 -0
  17. package/examples/chat-input-submit-on-enter-demo.d.ts +1 -0
  18. package/examples/chat-input-submit-on-enter-demo.js +23 -0
  19. package/examples/chat-input-submit-on-enter-demo.js.map +1 -0
  20. package/examples/chat-message-actions-demo.d.ts +1 -0
  21. package/examples/chat-message-actions-demo.js +29 -0
  22. package/examples/chat-message-actions-demo.js.map +1 -0
  23. package/examples/chat-message-demo.js +1 -0
  24. package/examples/chat-message-demo.js.map +1 -1
  25. package/examples/chat-message-editor-demo.d.ts +1 -0
  26. package/examples/chat-message-editor-demo.js +30 -0
  27. package/examples/chat-message-editor-demo.js.map +1 -0
  28. package/examples/chat-message-with-actions-demo.d.ts +1 -0
  29. package/examples/chat-message-with-actions-demo.js +26 -0
  30. package/examples/chat-message-with-actions-demo.js.map +1 -0
  31. package/examples/chat-typing-indicator-demo.js +2 -1
  32. package/examples/chat-typing-indicator-demo.js.map +1 -1
  33. package/examples/chat-window-csat-demo.js +2 -1
  34. package/examples/chat-window-csat-demo.js.map +1 -1
  35. package/examples/chat-window-csat-prompt-demo.js +2 -1
  36. package/examples/chat-window-csat-prompt-demo.js.map +1 -1
  37. package/examples/chat-window-demo.js +2 -1
  38. package/examples/chat-window-demo.js.map +1 -1
  39. package/examples/chat-window-quote-demo.js +2 -1
  40. package/examples/chat-window-quote-demo.js.map +1 -1
  41. package/examples/chat-window-quotes-demo.js +2 -1
  42. package/examples/chat-window-quotes-demo.js.map +1 -1
  43. package/package.json +1 -1
  44. package/ui/ChatBody.d.ts +1 -25
  45. package/ui/ChatBody.js +2 -88
  46. package/ui/ChatButton.d.ts +1 -6
  47. package/ui/ChatButton.js +2 -33
  48. package/ui/ChatHeader.d.ts +1 -6
  49. package/ui/ChatHeader.js +2 -43
  50. package/ui/ChatInput.d.ts +1 -10
  51. package/ui/ChatInput.js +2 -148
  52. package/ui/ChatMessage.d.ts +1 -6
  53. package/ui/ChatMessage.js +1 -0
  54. package/ui/ChatTypingIndicator.d.ts +1 -2
  55. package/ui/ChatTypingIndicator.js +2 -30
  56. package/ui/ChatWindow.d.ts +1 -34
  57. package/ui/ChatWindow.js +2 -57
  58. package/ui/ChatWindowCsatPrompt.d.ts +1 -15
  59. package/ui/ChatWindowCsatPrompt.js +2 -70
  60. package/ui/ChatWindowCsatThankYou.d.ts +1 -9
  61. package/ui/ChatWindowCsatThankYou.js +2 -28
  62. package/ui/ChatWindowQuote.d.ts +1 -22
  63. package/ui/ChatWindowQuote.js +2 -140
  64. package/ui/chat/ChatBody.d.ts +31 -0
  65. package/ui/chat/ChatBody.js +92 -0
  66. package/ui/chat/ChatBody.js.map +1 -0
  67. package/ui/chat/ChatButton.d.ts +6 -0
  68. package/ui/chat/ChatButton.js +33 -0
  69. package/ui/chat/ChatButton.js.map +1 -0
  70. package/ui/chat/ChatHeader.d.ts +6 -0
  71. package/ui/chat/ChatHeader.js +43 -0
  72. package/ui/chat/ChatHeader.js.map +1 -0
  73. package/ui/chat/ChatInput.d.ts +28 -0
  74. package/ui/chat/ChatInput.js +165 -0
  75. package/ui/chat/ChatInput.js.map +1 -0
  76. package/ui/chat/ChatMessage.d.ts +17 -0
  77. package/ui/chat/ChatMessage.js +2 -0
  78. package/ui/chat/ChatMessageActions.d.ts +23 -0
  79. package/ui/chat/ChatMessageActions.js +69 -0
  80. package/ui/chat/ChatMessageActions.js.map +1 -0
  81. package/ui/chat/ChatMessageEditor.d.ts +9 -0
  82. package/ui/chat/ChatMessageEditor.js +57 -0
  83. package/ui/chat/ChatMessageEditor.js.map +1 -0
  84. package/ui/chat/ChatTypingIndicator.d.ts +2 -0
  85. package/ui/chat/ChatTypingIndicator.js +30 -0
  86. package/ui/chat/ChatTypingIndicator.js.map +1 -0
  87. package/ui/chat/ChatWindow.d.ts +37 -0
  88. package/ui/chat/ChatWindow.js +65 -0
  89. package/ui/chat/ChatWindow.js.map +1 -0
  90. package/ui/chat/ChatWindowCsatPrompt.d.ts +15 -0
  91. package/ui/chat/ChatWindowCsatPrompt.js +71 -0
  92. package/ui/chat/ChatWindowCsatPrompt.js.map +1 -0
  93. package/ui/chat/ChatWindowCsatThankYou.d.ts +9 -0
  94. package/ui/chat/ChatWindowCsatThankYou.js +29 -0
  95. package/ui/chat/ChatWindowCsatThankYou.js.map +1 -0
  96. package/ui/chat/ChatWindowQuote.d.ts +22 -0
  97. package/ui/chat/ChatWindowQuote.js +141 -0
  98. package/ui/chat/ChatWindowQuote.js.map +1 -0
  99. package/ui/ChatBody.js.map +0 -1
  100. package/ui/ChatButton.js.map +0 -1
  101. package/ui/ChatHeader.js.map +0 -1
  102. package/ui/ChatInput.js.map +0 -1
  103. package/ui/ChatTypingIndicator.js.map +0 -1
  104. package/ui/ChatWindow.js.map +0 -1
  105. package/ui/ChatWindowCsatPrompt.js.map +0 -1
  106. package/ui/ChatWindowCsatThankYou.js.map +0 -1
  107. package/ui/ChatWindowQuote.js.map +0 -1
@@ -1,141 +1,3 @@
1
1
  "use client";
2
- import { Button as e } from "./buttons/Button.js";
3
- import { twMerge as t } from "./helpers/twMerge.js";
4
- import { Scrollbar as n } from "./Scrollbar.js";
5
- import { jsx as r, jsxs as i } from "react/jsx-runtime";
6
- //#region lib/ui/ChatWindowQuote.tsx
7
- function a({ quote: t, onCtaClick: n }) {
8
- return /* @__PURE__ */ i("article", {
9
- "aria-label": t.resortName,
10
- className: "flex flex-col bg-white border border-lightGrey rounded-32 shadow-[0px_2px_14px_0px_rgba(0,0,0,0.25)] overflow-hidden p-24 gap-16",
11
- children: [/* @__PURE__ */ i("div", {
12
- className: "relative h-210 w-full overflow-hidden rounded-16",
13
- children: [
14
- /* @__PURE__ */ r("img", {
15
- src: t.imageUrl,
16
- alt: "",
17
- className: "absolute inset-0 size-full object-cover"
18
- }),
19
- /* @__PURE__ */ r("div", { className: "absolute inset-0 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]" }),
20
- /* @__PURE__ */ i("div", {
21
- className: "absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-20",
22
- children: [/* @__PURE__ */ r("p", {
23
- className: "text-b2 font-semibold",
24
- children: t.resortName
25
- }), /* @__PURE__ */ r("p", {
26
- className: "text-b3",
27
- children: t.country
28
- })]
29
- })
30
- ]
31
- }), /* @__PURE__ */ i("div", {
32
- className: "flex flex-col gap-16 items-center",
33
- children: [
34
- /* @__PURE__ */ i("p", {
35
- className: "text-b4 font-semibold text-black text-center",
36
- children: [
37
- t.resortName,
38
- ", ",
39
- t.dateRange,
40
- ", ",
41
- t.pax
42
- ]
43
- }),
44
- /* @__PURE__ */ r(e, {
45
- color: "saffron",
46
- onClick: () => n?.(t),
47
- children: t.ctaLabel ?? "See prices"
48
- }),
49
- t.packageIncludes && t.packageIncludes.length > 0 && /* @__PURE__ */ i("div", {
50
- className: "flex flex-col gap-4 w-full",
51
- children: [/* @__PURE__ */ r("p", {
52
- className: "text-b3 font-semibold text-black",
53
- children: "Your All-Inclusive package includes:"
54
- }), /* @__PURE__ */ r("ul", {
55
- className: "flex flex-col gap-2",
56
- children: t.packageIncludes.map((e) => /* @__PURE__ */ i("li", {
57
- className: "text-b3 text-black",
58
- children: ["• ", e]
59
- }, e))
60
- })]
61
- }),
62
- t.additionalInfo && /* @__PURE__ */ r("p", {
63
- className: "text-b3 text-black",
64
- children: t.additionalInfo
65
- })
66
- ]
67
- })]
68
- });
69
- }
70
- function o({ quote: t, onCtaClick: n }) {
71
- return /* @__PURE__ */ i("article", {
72
- "aria-label": t.resortName,
73
- className: "flex flex-col gap-12 items-center shrink-0 w-200",
74
- children: [/* @__PURE__ */ i("div", {
75
- className: "relative size-200 rounded-16 overflow-hidden",
76
- children: [
77
- /* @__PURE__ */ r("img", {
78
- src: t.imageUrl,
79
- alt: "",
80
- className: "absolute inset-0 size-full object-cover"
81
- }),
82
- /* @__PURE__ */ r("div", { className: "absolute inset-0 rounded-16 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]" }),
83
- /* @__PURE__ */ i("div", {
84
- className: "absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-8",
85
- children: [/* @__PURE__ */ r("p", {
86
- className: "text-b4 font-semibold",
87
- children: t.resortName
88
- }), /* @__PURE__ */ r("p", {
89
- className: "text-b5",
90
- children: t.country
91
- })]
92
- })
93
- ]
94
- }), /* @__PURE__ */ i("div", {
95
- className: "flex flex-col gap-8 items-center",
96
- children: [/* @__PURE__ */ i("p", {
97
- className: "text-b5 font-semibold text-black text-center",
98
- children: [
99
- t.resortName,
100
- /* @__PURE__ */ r("br", {}),
101
- t.dateRange,
102
- ",",
103
- /* @__PURE__ */ r("br", {}),
104
- t.pax
105
- ]
106
- }), /* @__PURE__ */ r(e, {
107
- color: "black",
108
- onClick: () => n?.(t),
109
- children: t.ctaLabel ?? "See prices"
110
- })]
111
- })]
112
- });
113
- }
114
- function s({ quotes: e, onCtaClick: i, className: s }) {
115
- return e.length === 0 ? null : e.length === 1 ? /* @__PURE__ */ r("div", {
116
- className: t(s),
117
- children: /* @__PURE__ */ r(a, {
118
- quote: e[0],
119
- onCtaClick: i
120
- })
121
- }) : /* @__PURE__ */ r("div", {
122
- className: t("flex flex-col gap-8", s),
123
- children: /* @__PURE__ */ r(n, {
124
- timelineName: "--chat-quote-scroller",
125
- scrollerClassName: "pb-24",
126
- scrollbarWidth: "60%",
127
- "aria-label": "Quote options",
128
- children: /* @__PURE__ */ r("div", {
129
- className: "flex gap-12",
130
- children: e.map((e) => /* @__PURE__ */ r(o, {
131
- quote: e,
132
- onCtaClick: i
133
- }, e.id))
134
- })
135
- })
136
- });
137
- }
138
- //#endregion
139
- export { s as ChatWindowQuote };
140
-
141
- //# sourceMappingURL=ChatWindowQuote.js.map
2
+ import { ChatWindowQuote as e } from "./chat/ChatWindowQuote.js";
3
+ export { e as ChatWindowQuote };
@@ -0,0 +1,31 @@
1
+ import { ChatMessageSender } from './ChatMessage';
2
+ import { ChatMessageLabels } from './ChatMessageActions';
3
+ import { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';
4
+ import { ChatWindowCsatThankYouLabels } from './ChatWindowCsatThankYou';
5
+ import { ChatWindowQuoteItem } from './ChatWindowQuote';
6
+ import { HTMLAttributes, ReactNode } from 'react';
7
+ export interface ChatMessage {
8
+ id: string;
9
+ sender: ChatMessageSender;
10
+ content: ReactNode;
11
+ }
12
+ export interface ChatBodyProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
13
+ greeting?: string;
14
+ messages: ChatMessage[];
15
+ isTyping?: boolean;
16
+ /** Actions of the user messages (replay, edit, copy), shown when a message is hovered or focused */
17
+ onMessageReplay?: (message: ChatMessage) => void;
18
+ onMessageEdit?: (message: ChatMessage, content: string) => void;
19
+ onMessageCopy?: (message: ChatMessage) => void | Promise<void>;
20
+ messageActionLabels?: Partial<ChatMessageLabels>;
21
+ onFeedbackRate?: (rating: number) => void;
22
+ csatLabels?: ChatWindowCsatPromptLabels;
23
+ csatRenderRatingLabel?: (rating: number) => ReactNode;
24
+ csatThankYouLabels?: ChatWindowCsatThankYouLabels;
25
+ onQuoteCtaClick?: (quote: ChatWindowQuoteItem) => void;
26
+ }
27
+ export interface ChatHandle {
28
+ askForFeedback: () => void;
29
+ showQuotes: (quotes: ChatWindowQuoteItem[]) => void;
30
+ }
31
+ export declare const ChatBody: import('react').ForwardRefExoticComponent<ChatBodyProps & import('react').RefAttributes<ChatHandle>>;
@@ -0,0 +1,92 @@
1
+ import { twMerge as e } from "../helpers/twMerge.js";
2
+ import { t } from "../../chunks/ChatMessage.js";
3
+ import { ChatTypingIndicator as n } from "./ChatTypingIndicator.js";
4
+ import { ChatWindowCsatPrompt as r } from "./ChatWindowCsatPrompt.js";
5
+ import { ChatWindowCsatThankYou as i } from "./ChatWindowCsatThankYou.js";
6
+ import { ChatWindowQuote as a } from "./ChatWindowQuote.js";
7
+ import { forwardRef as o, useEffect as s, useImperativeHandle as c, useRef as l, useState as u } from "react";
8
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
9
+ //#region lib/ui/chat/ChatBody.tsx
10
+ function p({ messages: e, isTyping: t, csatRenderRatingLabel: n, onFeedbackRate: r, csatLabels: i }, a) {
11
+ let o = l(null), [d, f] = u(!1), [p, m] = u(null), [h, g] = u([]);
12
+ c(a, () => ({
13
+ askForFeedback() {
14
+ f(!0);
15
+ },
16
+ showQuotes(e) {
17
+ g((t) => [...t, e]);
18
+ }
19
+ })), s(() => {
20
+ let e = o.current;
21
+ e && e.scrollTo({
22
+ top: e.scrollHeight,
23
+ behavior: "smooth"
24
+ });
25
+ }, [
26
+ e,
27
+ t,
28
+ d,
29
+ p,
30
+ h
31
+ ]);
32
+ function _(e) {
33
+ m(String(n ? n(e) : e)), r?.(e);
34
+ }
35
+ return {
36
+ logRef: o,
37
+ csatRatingMessage: p,
38
+ quoteGroups: h,
39
+ handleRate: _,
40
+ csatReady: d && i && n
41
+ };
42
+ }
43
+ var m = o(function(o, s) {
44
+ let { logRef: c, csatRatingMessage: l, quoteGroups: u, handleRate: m, csatReady: h } = p(o, s), { greeting: g, messages: _, isTyping: v = !1, onMessageReplay: y, onMessageEdit: b, onMessageCopy: x, messageActionLabels: S, csatLabels: C, csatRenderRatingLabel: w, csatThankYouLabels: T, onQuoteCtaClick: E, className: D, ...O } = o;
45
+ return /* @__PURE__ */ f("div", {
46
+ ...O,
47
+ ref: c,
48
+ role: "log",
49
+ "aria-label": "Chat messages",
50
+ "aria-live": "polite",
51
+ className: e("chat-message-log flex flex-col flex-1 gap-20 overflow-y-auto py-20", D),
52
+ children: [
53
+ g ? /* @__PURE__ */ d("div", {
54
+ className: "px-20",
55
+ children: /* @__PURE__ */ d("p", {
56
+ className: "text-b3 text-black",
57
+ children: g
58
+ })
59
+ }) : null,
60
+ _.map((e) => /* @__PURE__ */ d(t, {
61
+ sender: e.sender,
62
+ onReplay: y && (() => y(e)),
63
+ onEdit: b && ((t) => b(e, t)),
64
+ onCopy: x && (() => x(e)),
65
+ labels: S,
66
+ children: e.content
67
+ }, e.id)),
68
+ v && /* @__PURE__ */ d("div", {
69
+ className: "px-12",
70
+ children: /* @__PURE__ */ d(n, {})
71
+ }),
72
+ h && /* @__PURE__ */ d(r, {
73
+ onRate: m,
74
+ labels: C,
75
+ renderRatingLabel: w
76
+ }),
77
+ l && /* @__PURE__ */ d(t, {
78
+ sender: "user",
79
+ children: l
80
+ }),
81
+ l && T && /* @__PURE__ */ d(i, { labels: T }),
82
+ u.map((e, t) => /* @__PURE__ */ d(a, {
83
+ quotes: e,
84
+ onCtaClick: E
85
+ }, t))
86
+ ]
87
+ });
88
+ });
89
+ //#endregion
90
+ export { m as ChatBody };
91
+
92
+ //# sourceMappingURL=ChatBody.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatBody.js","names":[],"sources":["../../../lib/ui/chat/ChatBody.tsx"],"sourcesContent":["import { ChatMessage, type ChatMessageSender } from './ChatMessage';\nimport type { ChatMessageLabels } from './ChatMessageActions';\nimport { ChatTypingIndicator } from './ChatTypingIndicator';\nimport type { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';\nimport { ChatWindowCsatPrompt } from './ChatWindowCsatPrompt';\nimport {\n ChatWindowCsatThankYou,\n type ChatWindowCsatThankYouLabels,\n} from './ChatWindowCsatThankYou';\nimport { ChatWindowQuote, type ChatWindowQuoteItem } from './ChatWindowQuote';\nimport {\n forwardRef,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { twMerge } from '../helpers/twMerge';\n\nexport interface ChatMessage {\n id: string;\n sender: ChatMessageSender;\n content: ReactNode;\n}\n\nexport interface ChatBodyProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n greeting?: string;\n messages: ChatMessage[];\n isTyping?: boolean;\n /** Actions of the user messages (replay, edit, copy), shown when a message is hovered or focused */\n onMessageReplay?: (message: ChatMessage) => void;\n onMessageEdit?: (message: ChatMessage, content: string) => void;\n onMessageCopy?: (message: ChatMessage) => void | Promise<void>;\n messageActionLabels?: Partial<ChatMessageLabels>;\n onFeedbackRate?: (rating: number) => void;\n csatLabels?: ChatWindowCsatPromptLabels;\n csatRenderRatingLabel?: (rating: number) => ReactNode;\n csatThankYouLabels?: ChatWindowCsatThankYouLabels;\n onQuoteCtaClick?: (quote: ChatWindowQuoteItem) => void;\n}\n\nexport interface ChatHandle {\n askForFeedback: () => void;\n showQuotes: (quotes: ChatWindowQuoteItem[]) => void;\n}\n\nfunction useChat(\n { messages, isTyping, csatRenderRatingLabel, onFeedbackRate, csatLabels }: ChatBodyProps,\n ref: Ref<ChatHandle>,\n) {\n const logRef = useRef<HTMLDivElement>(null);\n const [showCsat, setShowCsat] = useState(false);\n const [csatRatingMessage, setCsatRatingMessage] = useState<string | null>(null);\n const [quoteGroups, setQuoteGroups] = useState<ChatWindowQuoteItem[][]>([]);\n\n useImperativeHandle(ref, () => ({\n askForFeedback() {\n setShowCsat(true);\n },\n showQuotes(quotes: ChatWindowQuoteItem[]) {\n setQuoteGroups((prev) => [...prev, quotes]);\n },\n }));\n\n useEffect(() => {\n const el = logRef.current;\n if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });\n }, [messages, isTyping, showCsat, csatRatingMessage, quoteGroups]);\n\n function handleRate(rating: number) {\n const label = csatRenderRatingLabel ? String(csatRenderRatingLabel(rating)) : String(rating);\n setCsatRatingMessage(label);\n onFeedbackRate?.(rating);\n }\n\n const csatReady = showCsat && csatLabels && csatRenderRatingLabel;\n\n return { logRef, csatRatingMessage, quoteGroups, handleRate, csatReady };\n}\n\nexport const ChatBody = forwardRef<ChatHandle, ChatBodyProps>(function Chat(props, ref) {\n const { logRef, csatRatingMessage, quoteGroups, handleRate, csatReady } = useChat(props, ref);\n\n const {\n greeting,\n messages,\n isTyping = false,\n onMessageReplay,\n onMessageEdit,\n onMessageCopy,\n messageActionLabels,\n csatLabels,\n csatRenderRatingLabel,\n csatThankYouLabels,\n onQuoteCtaClick,\n className,\n ...extraProps\n } = props;\n\n return (\n <div\n {...extraProps}\n ref={logRef}\n role=\"log\"\n aria-label=\"Chat messages\"\n aria-live=\"polite\"\n className={twMerge(\n 'chat-message-log flex flex-col flex-1 gap-20 overflow-y-auto py-20',\n className,\n )}\n >\n {greeting ? (\n <div className=\"px-20\">\n <p className=\"text-b3 text-black\">{greeting}</p>\n </div>\n ) : null}\n\n {messages.map((msg) => (\n <ChatMessage\n key={msg.id}\n sender={msg.sender}\n onReplay={onMessageReplay && (() => onMessageReplay(msg))}\n onEdit={onMessageEdit && ((content) => onMessageEdit(msg, content))}\n onCopy={onMessageCopy && (() => onMessageCopy(msg))}\n labels={messageActionLabels}\n >\n {msg.content}\n </ChatMessage>\n ))}\n\n {isTyping && (\n <div className=\"px-12\">\n <ChatTypingIndicator />\n </div>\n )}\n\n {csatReady && (\n <ChatWindowCsatPrompt\n onRate={handleRate}\n labels={csatLabels!}\n renderRatingLabel={csatRenderRatingLabel!}\n />\n )}\n\n {csatRatingMessage && <ChatMessage sender=\"user\">{csatRatingMessage}</ChatMessage>}\n\n {csatRatingMessage && csatThankYouLabels && (\n <ChatWindowCsatThankYou labels={csatThankYouLabels} />\n )}\n\n {quoteGroups.map((quotes, idx) => (\n <ChatWindowQuote key={idx} quotes={quotes} onCtaClick={onQuoteCtaClick} />\n ))}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;AAiDA,SAAS,EACP,EAAE,aAAU,aAAU,0BAAuB,mBAAgB,iBAC7D,GACA;CACA,IAAM,IAAS,EAAuB,KAAK,EACrC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAmB,KAAwB,EAAwB,KAAK,EACzE,CAAC,GAAa,KAAkB,EAAkC,EAAE,CAAC;AAW3E,CATA,EAAoB,UAAY;EAC9B,iBAAiB;AACf,KAAY,GAAK;;EAEnB,WAAW,GAA+B;AACxC,MAAgB,MAAS,CAAC,GAAG,GAAM,EAAO,CAAC;;EAE9C,EAAE,EAEH,QAAgB;EACd,IAAM,IAAK,EAAO;AAClB,EAAI,KAAI,EAAG,SAAS;GAAE,KAAK,EAAG;GAAc,UAAU;GAAU,CAAC;IAChE;EAAC;EAAU;EAAU;EAAU;EAAmB;EAAY,CAAC;CAElE,SAAS,EAAW,GAAgB;AAGlC,EADA,EADsC,OAAxB,IAA+B,EAAsB,EAAO,GAAW,EAAO,CACjE,EAC3B,IAAiB,EAAO;;AAK1B,QAAO;EAAE;EAAQ;EAAmB;EAAa;EAAY,WAF3C,KAAY,KAAc;EAE4B;;AAG1E,IAAa,IAAW,EAAsC,SAAc,GAAO,GAAK;CACtF,IAAM,EAAE,WAAQ,sBAAmB,gBAAa,eAAY,iBAAc,EAAQ,GAAO,EAAI,EAEvF,EACJ,aACA,aACA,cAAW,IACX,oBACA,kBACA,kBACA,wBACA,eACA,0BACA,uBACA,oBACA,cACA,GAAG,MACD;AAEJ,QACE,kBAAC,OAAD;EACE,GAAI;EACJ,KAAK;EACL,MAAK;EACL,cAAW;EACX,aAAU;EACV,WAAW,EACT,sEACA,EACD;YATH;GAWG,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,KAAD;KAAG,WAAU;eAAsB;KAAa,CAAA;IAC5C,CAAA,GACJ;GAEH,EAAS,KAAK,MACb,kBAAC,GAAD;IAEE,QAAQ,EAAI;IACZ,UAAU,YAA0B,EAAgB,EAAI;IACxD,QAAQ,OAAmB,MAAY,EAAc,GAAK,EAAQ;IAClE,QAAQ,YAAwB,EAAc,EAAI;IAClD,QAAQ;cAEP,EAAI;IACO,EARP,EAAI,GAQG,CACd;GAED,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAuB,CAAA;IACnB,CAAA;GAGP,KACC,kBAAC,GAAD;IACE,QAAQ;IACR,QAAQ;IACR,mBAAmB;IACnB,CAAA;GAGH,KAAqB,kBAAC,GAAD;IAAa,QAAO;cAAQ;IAAgC,CAAA;GAEjF,KAAqB,KACpB,kBAAC,GAAD,EAAwB,QAAQ,GAAsB,CAAA;GAGvD,EAAY,KAAK,GAAQ,MACxB,kBAAC,GAAD;IAAmC;IAAQ,YAAY;IAAmB,EAApD,EAAoD,CAC1E;GACE;;EAER"}
@@ -0,0 +1,6 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+ export type ChatButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
3
+ offset?: boolean;
4
+ unfoldOn?: 'hover' | 'active';
5
+ };
6
+ export declare function ChatButton({ children, className, type, 'aria-label': ariaLabel, tabIndex, offset, unfoldOn, ...rest }: ChatButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { twMerge as e } from "../helpers/twMerge.js";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
+ import { Icon as r } from "@clubmed/trident-icons";
4
+ //#region lib/ui/chat/ChatButton.tsx
5
+ var i = "AI Agent: Have you got a question?", a = "Chat with your assistant";
6
+ function o({ children: o = i, className: s, type: c = "button", "aria-label": l, tabIndex: u, offset: d, unfoldOn: f = "hover", ...p }) {
7
+ let m = l ?? (typeof o == "string" && o.trim().length > 0 ? o.trim() : a);
8
+ return /* @__PURE__ */ n("button", {
9
+ type: c,
10
+ "aria-label": m,
11
+ tabIndex: u ?? 0,
12
+ ...p,
13
+ className: e("chat-border grid grid-cols-[24px_0fr] transition-[grid-template-columns] justify-center items-center overflow-hidden cursor-pointer transition-all rounded-pill px-8 md:px-16 outline-none group/Chatbutton focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 pointer-events-auto h-48 min-h-48 md:min-w-80 md:h-80 active:scale-[0.98]", d && "md:translate-x-[40px] focus-visible:translate-x-0", f === "hover" && "group/hoverChatbutton hover:grid-cols-[24px_1fr] hover:translate-x-0", f === "active" && "group/activeChatbutton active:grid-cols-[24px_1fr] active:translate-x-0", "focus-visible:grid-cols-[24px_1fr]", s),
14
+ children: [/* @__PURE__ */ t(r, {
15
+ name: "Sparkles",
16
+ className: e("chat-button-icon w-24 transition-all", d && "relative md:start-[calc(-16px)] group-focus-visible/Chatbutton:start-0 group-hover/hoverChatbutton:start-0 group-active/activeChatbutton:start-0")
17
+ }), /* @__PURE__ */ t("div", {
18
+ className: "grid grid-cols-[0fr] overflow-hidden opacity-0 transition-opacity group-hover/hoverChatbutton:opacity-100 group-focus-visible/Chatbutton:opacity-100 group-active/activeChatbutton:opacity-100 duration-500",
19
+ children: /* @__PURE__ */ n("span", {
20
+ "aria-hidden": "true",
21
+ className: "chat-button-text items-center ps-20 flex gap-10 whitespace-nowrap font-semibold text-b3 text-black",
22
+ children: [o, /* @__PURE__ */ t(r, {
23
+ name: "ArrowTailRight",
24
+ width: "24px"
25
+ })]
26
+ })
27
+ })]
28
+ });
29
+ }
30
+ //#endregion
31
+ export { o as ChatButton };
32
+
33
+ //# sourceMappingURL=ChatButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatButton.js","names":[],"sources":["../../../lib/ui/chat/ChatButton.tsx"],"sourcesContent":["import { Icon } from '@clubmed/trident-icons';\nimport type { ButtonHTMLAttributes } from 'react';\nimport { twMerge } from '../helpers/twMerge';\n\nconst DEFAULT_TEASER_COPY = 'AI Agent: Have you got a question?';\nconst FALLBACK_ARIA_LABEL = 'Chat with your assistant';\n\nexport type ChatButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n offset?: boolean;\n unfoldOn?: 'hover' | 'active';\n};\n\nexport function ChatButton({\n children = DEFAULT_TEASER_COPY,\n className,\n type = 'button',\n 'aria-label': ariaLabel,\n tabIndex,\n offset,\n unfoldOn = 'hover',\n ...rest\n}: ChatButtonProps) {\n const label =\n ariaLabel ??\n (typeof children === 'string' && children.trim().length > 0\n ? children.trim()\n : FALLBACK_ARIA_LABEL);\n\n return (\n <button\n type={type}\n aria-label={label}\n tabIndex={tabIndex ?? 0}\n {...rest}\n className={twMerge(\n 'chat-border grid grid-cols-[24px_0fr] transition-[grid-template-columns] justify-center items-center overflow-hidden cursor-pointer transition-all rounded-pill px-8 md:px-16 outline-none group/Chatbutton focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 pointer-events-auto h-48 min-h-48 md:min-w-80 md:h-80 active:scale-[0.98]',\n offset && 'md:translate-x-[40px] focus-visible:translate-x-0',\n unfoldOn === 'hover' &&\n 'group/hoverChatbutton hover:grid-cols-[24px_1fr] hover:translate-x-0',\n unfoldOn === 'active' &&\n 'group/activeChatbutton active:grid-cols-[24px_1fr] active:translate-x-0',\n 'focus-visible:grid-cols-[24px_1fr]',\n className,\n )}\n >\n <Icon\n name=\"Sparkles\"\n className={twMerge(\n 'chat-button-icon w-24 transition-all',\n offset &&\n 'relative md:start-[calc(-16px)] group-focus-visible/Chatbutton:start-0 group-hover/hoverChatbutton:start-0 group-active/activeChatbutton:start-0',\n )}\n />\n <div className=\"grid grid-cols-[0fr] overflow-hidden opacity-0 transition-opacity group-hover/hoverChatbutton:opacity-100 group-focus-visible/Chatbutton:opacity-100 group-active/activeChatbutton:opacity-100 duration-500\">\n <span\n aria-hidden=\"true\"\n className=\"chat-button-text items-center ps-20 flex gap-10 whitespace-nowrap font-semibold text-b3 text-black\"\n >\n {children}\n <Icon name=\"ArrowTailRight\" width=\"24px\" />\n </span>\n </div>\n </button>\n );\n}\n"],"mappings":";;;;AAIA,IAAM,IAAsB,sCACtB,IAAsB;AAO5B,SAAgB,EAAW,EACzB,cAAW,GACX,cACA,UAAO,UACP,cAAc,GACd,aACA,WACA,cAAW,SACX,GAAG,KACe;CAClB,IAAM,IACJ,MACC,OAAO,KAAa,YAAY,EAAS,MAAM,CAAC,SAAS,IACtD,EAAS,MAAM,GACf;AAEN,QACE,kBAAC,UAAD;EACQ;EACN,cAAY;EACZ,UAAU,KAAY;EACtB,GAAI;EACJ,WAAW,EACT,mWACA,KAAU,qDACV,MAAa,WACX,wEACF,MAAa,YACX,2EACF,sCACA,EACD;YAdH,CAgBE,kBAAC,GAAD;GACE,MAAK;GACL,WAAW,EACT,wCACA,KACE,mJACH;GACD,CAAA,EACF,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,QAAD;IACE,eAAY;IACZ,WAAU;cAFZ,CAIG,GACD,kBAAC,GAAD;KAAM,MAAK;KAAiB,OAAM;KAAS,CAAA,CACtC;;GACH,CAAA,CACC"}
@@ -0,0 +1,6 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ChatHeaderProps {
3
+ title?: string;
4
+ logo?: ReactNode;
5
+ }
6
+ export declare function ChatHeader({ title, logo }: ChatHeaderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { jsx as e, jsxs as t } from "react/jsx-runtime";
2
+ import { Icon as n } from "@clubmed/trident-icons";
3
+ //#region lib/ui/chat/ChatHeader.tsx
4
+ var r = /* @__PURE__ */ e("svg", {
5
+ className: "absolute top-full inset-x-[50%] -translate-x-1/2",
6
+ width: "118",
7
+ height: "27",
8
+ viewBox: "0 0 118 27",
9
+ children: /* @__PURE__ */ e("path", {
10
+ d: "M0 0C10.8283 0 21.1946 5.40419 28.7327 13.2062L30.2673 14.7945C45.991 31.0685 72.009 31.0685 87.7327 14.7945L89.2673 13.2062C96.8054 5.40419 107.172 0 118 0C118 0 80.6853 0 0 0Z",
11
+ fill: "#F6EFE7"
12
+ })
13
+ });
14
+ function i({ title: i = "Ask me a question", logo: a }) {
15
+ return /* @__PURE__ */ t("div", {
16
+ className: "relative flex flex-col items-center shrink-0 pt-4 px-4 h-118 overflow-hidden",
17
+ children: [/* @__PURE__ */ e("div", {
18
+ className: "absolute inset-x-4 top-4 h-87 bg-lightSand rounded-[24px]",
19
+ children: r
20
+ }), /* @__PURE__ */ t("div", {
21
+ className: "relative flex flex-col flex-1 items-center justify-end pb-8 w-full z-10",
22
+ children: [/* @__PURE__ */ e("div", {
23
+ className: "flex items-center justify-center px-12 py-8 w-full",
24
+ children: /* @__PURE__ */ e("p", {
25
+ className: "font-serif font-bold italic text-h5 text-center text-black line-clamp-2",
26
+ children: i
27
+ })
28
+ }), a ?? /* @__PURE__ */ e("button", {
29
+ type: "button",
30
+ "aria-label": "AI assistant",
31
+ className: "chat-border flex items-center justify-center size-48 rounded-full bg-white outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2",
32
+ children: /* @__PURE__ */ e(n, {
33
+ name: "Sparkles",
34
+ width: "24px"
35
+ })
36
+ })]
37
+ })]
38
+ });
39
+ }
40
+ //#endregion
41
+ export { i as ChatHeader };
42
+
43
+ //# sourceMappingURL=ChatHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatHeader.js","names":[],"sources":["../../../lib/ui/chat/ChatHeader.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Icon } from '@clubmed/trident-icons';\n\nexport interface ChatHeaderProps {\n title?: string;\n logo?: ReactNode;\n}\n\nconst headerCurve = (\n <svg\n className=\"absolute top-full inset-x-[50%] -translate-x-1/2\"\n width=\"118\"\n height=\"27\"\n viewBox=\"0 0 118 27\"\n >\n <path\n d=\"M0 0C10.8283 0 21.1946 5.40419 28.7327 13.2062L30.2673 14.7945C45.991 31.0685 72.009 31.0685 87.7327 14.7945L89.2673 13.2062C96.8054 5.40419 107.172 0 118 0C118 0 80.6853 0 0 0Z\"\n fill=\"#F6EFE7\"\n />\n </svg>\n);\n\nexport function ChatHeader({ title = 'Ask me a question', logo }: ChatHeaderProps) {\n return (\n <div className=\"relative flex flex-col items-center shrink-0 pt-4 px-4 h-118 overflow-hidden\">\n <div className=\"absolute inset-x-4 top-4 h-87 bg-lightSand rounded-[24px]\">{headerCurve}</div>\n <div className=\"relative flex flex-col flex-1 items-center justify-end pb-8 w-full z-10\">\n <div className=\"flex items-center justify-center px-12 py-8 w-full\">\n <p className=\"font-serif font-bold italic text-h5 text-center text-black line-clamp-2\">\n {title}\n </p>\n </div>\n {logo ?? (\n <button\n type=\"button\"\n aria-label=\"AI assistant\"\n className=\"chat-border flex items-center justify-center size-48 rounded-full bg-white outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2\"\n >\n <Icon name=\"Sparkles\" width=\"24px\" />\n </button>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;AAQA,IAAM,IACJ,kBAAC,OAAD;CACE,WAAU;CACV,OAAM;CACN,QAAO;CACP,SAAQ;WAER,kBAAC,QAAD;EACE,GAAE;EACF,MAAK;EACL,CAAA;CACE,CAAA;AAGR,SAAgB,EAAW,EAAE,WAAQ,qBAAqB,WAAyB;AACjF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAA6D;GAAkB,CAAA,EAC9F,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,KAAD;KAAG,WAAU;eACV;KACC,CAAA;IACA,CAAA,EACL,KACC,kBAAC,UAAD;IACE,MAAK;IACL,cAAW;IACX,WAAU;cAEV,kBAAC,GAAD;KAAM,MAAK;KAAW,OAAM;KAAS,CAAA;IAC9B,CAAA,CAEP;KACF"}
@@ -0,0 +1,28 @@
1
+ import { FormHTMLAttributes } from 'react';
2
+ /**
3
+ * Keyboard shortcut that submits the message:
4
+ * - `mod+enter`: Ctrl/Cmd+Enter submits, Enter adds a line break
5
+ * - `enter`: Enter submits, Shift+Enter adds a line break (Ctrl/Cmd+Enter submits too)
6
+ */
7
+ export type ChatInputSubmitKey = 'mod+enter' | 'enter';
8
+ export interface ChatInputProps extends FormHTMLAttributes<HTMLFormElement> {
9
+ value: string;
10
+ placeholder?: string;
11
+ onValueChange: (value: string) => void;
12
+ onSubmit: () => void;
13
+ /**
14
+ * Keyboard shortcut that submits the message.
15
+ * @default 'mod+enter'
16
+ */
17
+ submitKey?: ChatInputSubmitKey;
18
+ /** While `true`, submitting is disabled and the send button becomes a stop button */
19
+ isStreaming?: boolean;
20
+ /**
21
+ * Enables the microphone button. The browser speech recognition is used when available, this
22
+ * callback is the fallback for when the browser does not support the Web Speech API.
23
+ */
24
+ onAudioClick?: () => void;
25
+ /** Called when the stop button, shown in place of the send button while `isStreaming`, is clicked */
26
+ onStop?: () => void;
27
+ }
28
+ export declare function ChatInput({ value, placeholder, onValueChange, onSubmit, onAudioClick, onStop, isStreaming, submitKey, className, ...props }: ChatInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { Button as e } from "../buttons/Button.js";
3
+ import { twMerge as t } from "../helpers/twMerge.js";
4
+ import { useEffect as n, useRef as r, useState as i } from "react";
5
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
+ //#region lib/ui/chat/ChatInput.tsx
7
+ function s(e, t) {
8
+ return e.key !== "Enter" || e.nativeEvent.isComposing ? !1 : e.metaKey || e.ctrlKey ? !0 : t === "enter" && !e.shiftKey;
9
+ }
10
+ var c = 12, l = 48, u = 8;
11
+ function d(e) {
12
+ let t = r(e);
13
+ return n(() => {
14
+ t.current = e;
15
+ }), t;
16
+ }
17
+ var f = 3e3;
18
+ function p({ enabled: e, onValueChange: t }) {
19
+ let [a, o] = i(!1), [s, c] = i(!1), l = r(null), u = r(null), p = r(!1), m = d(t);
20
+ function h() {
21
+ u.current && clearTimeout(u.current);
22
+ }
23
+ function g() {
24
+ h(), u.current = setTimeout(() => {
25
+ p.current = !1, c(!1), l.current?.stop();
26
+ }, f);
27
+ }
28
+ n(() => {
29
+ let t = window.webkitSpeechRecognition ?? window.SpeechRecognition;
30
+ if (!e || !t) return;
31
+ o(!0);
32
+ let n = new t();
33
+ return n.continuous = !0, n.interimResults = !0, n.lang = navigator.language, n.onresult = (e) => {
34
+ let t = Array.from(e.results).map((e) => e[0].transcript).join("");
35
+ m.current(t), g();
36
+ }, n.onend = () => {
37
+ if (h(), p.current) try {
38
+ n.start();
39
+ } catch {}
40
+ else c(!1);
41
+ }, n.onerror = (e) => {
42
+ (e.error === "not-allowed" || e.error === "aborted") && (p.current = !1, c(!1), h());
43
+ }, l.current = n, () => {
44
+ n.abort(), h();
45
+ };
46
+ }, [e]);
47
+ function _() {
48
+ let e = l.current;
49
+ e && (p.current ? (p.current = !1, e.stop()) : (p.current = !0, c(!0), e.start(), g()));
50
+ }
51
+ return {
52
+ isSupported: a,
53
+ isListening: s,
54
+ toggle: _
55
+ };
56
+ }
57
+ function m(e) {
58
+ let t = r(null);
59
+ return n(() => {
60
+ let n = t.current;
61
+ if (!e) {
62
+ n && (n.style.height = "0px");
63
+ return;
64
+ }
65
+ let r, i = null, a = null;
66
+ return navigator.mediaDevices.getUserMedia({
67
+ audio: !0,
68
+ video: !1
69
+ }).then((e) => {
70
+ i = e, a = new AudioContext();
71
+ let n = a.createMediaStreamSource(i), o = a.createAnalyser();
72
+ o.fftSize = 256, n.connect(o);
73
+ let s = new Uint8Array(o.frequencyBinCount);
74
+ function c() {
75
+ o.getByteFrequencyData(s);
76
+ let e = s.reduce((e, t) => e + t, 0) / s.length, n = t.current;
77
+ n && (n.style.height = `${Math.sqrt(e / 255) * 10}px`), r = requestAnimationFrame(c);
78
+ }
79
+ r = requestAnimationFrame(c);
80
+ }), () => {
81
+ cancelAnimationFrame(r), i?.getTracks().forEach((e) => e.stop()), a?.close();
82
+ };
83
+ }, [e]), t;
84
+ }
85
+ function h({ value: n, placeholder: r = "Ask a question?", onValueChange: d, onSubmit: f, onAudioClick: h, onStop: g, isStreaming: _, submitKey: v = "mod+enter", className: y, ...b }) {
86
+ let { isSupported: x, isListening: S, toggle: C } = p({
87
+ enabled: !!h,
88
+ onValueChange: d
89
+ }), w = m(S), [T, E] = i(!1), D = n.trim(), O = T || D.length > 0, k = h ? l + u + l : l;
90
+ function A() {
91
+ x ? C() : h?.();
92
+ }
93
+ function j(e) {
94
+ e.preventDefault(), D && !_ && f();
95
+ }
96
+ function M(e) {
97
+ s(e, v) && (e.preventDefault(), D && !_ && f());
98
+ }
99
+ function N(e) {
100
+ e.target.tagName !== "TEXTAREA" && e.preventDefault();
101
+ }
102
+ return /* @__PURE__ */ o("form", {
103
+ onSubmit: j,
104
+ onMouseDown: N,
105
+ ...b,
106
+ style: { paddingBottom: O ? c + u + l : c },
107
+ className: t("relative flex items-start overflow-hidden bg-white border border-lightGrey p-12 w-full", "transition-[padding-bottom,border-radius] duration-250 ease-out rounded-b-[24px]", O ? "rounded-32" : "rounded-[40px]", y),
108
+ children: [/* @__PURE__ */ a("textarea", {
109
+ rows: O ? 3 : 1,
110
+ value: n,
111
+ onChange: (e) => d(e.target.value),
112
+ onKeyDown: M,
113
+ onFocus: () => E(!0),
114
+ onBlur: () => E(!1),
115
+ placeholder: r,
116
+ "aria-label": r,
117
+ style: { marginInlineEnd: O ? 0 : k + u },
118
+ className: t("scrollbar-hidden bg-transparent text-b3 py-12 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 resize-none w-full px-20 border border-middleGrey", "transition-[margin-inline-end,border-radius] duration-250 ease-out", O ? "text-black rounded-[20px]" : "text-darkGrey placeholder:text-darkGrey rounded-[40px]")
119
+ }), /* @__PURE__ */ o("div", {
120
+ style: {
121
+ insetInlineStart: O ? `calc(50% - ${k / 2}px)` : `calc(100% - ${c + k}px)`,
122
+ bottom: c
123
+ },
124
+ className: "absolute flex gap-8 items-center transition-[inset-inline-start] duration-250 ease-out",
125
+ children: [_ ? /* @__PURE__ */ a(e, {
126
+ type: "button",
127
+ "aria-label": "Stop generating",
128
+ onClick: g,
129
+ variant: "circle",
130
+ color: "black",
131
+ icon: "CrossDefault",
132
+ iconWidth: "24px"
133
+ }) : /* @__PURE__ */ a(e, {
134
+ type: "submit",
135
+ "aria-label": "Send message",
136
+ disabled: !D,
137
+ variant: "circle",
138
+ color: "saffron",
139
+ icon: "ArrowTailRight",
140
+ iconWidth: "24px"
141
+ }), !!h && /* @__PURE__ */ a(e, {
142
+ type: "button",
143
+ "aria-label": S ? "Stop recording" : "Use microphone",
144
+ "aria-pressed": S,
145
+ onClick: A,
146
+ variant: "circle",
147
+ theme: S ? "solid" : "outline",
148
+ color: S ? "sienna" : "black",
149
+ icon: "Audio",
150
+ iconWidth: "24px",
151
+ className: t("relative", S && "animate-pulse", !S && "border-lightGrey"),
152
+ children: /* @__PURE__ */ a("span", {
153
+ ref: w,
154
+ "aria-hidden": "true",
155
+ className: "bg-current absolute w-5 bottom-21",
156
+ style: { height: "0px" }
157
+ })
158
+ })]
159
+ })]
160
+ });
161
+ }
162
+ //#endregion
163
+ export { h as ChatInput };
164
+
165
+ //# sourceMappingURL=ChatInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatInput.js","names":[],"sources":["../../../lib/ui/chat/ChatInput.tsx"],"sourcesContent":["/// <reference types=\"@types/dom-speech-recognition\" />\n'use client';\n\nimport type { FormHTMLAttributes, KeyboardEvent, MouseEvent, SubmitEvent } from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Button } from '../buttons/Button';\nimport { twMerge } from '../helpers/twMerge';\n\n/**\n * Keyboard shortcut that submits the message:\n * - `mod+enter`: Ctrl/Cmd+Enter submits, Enter adds a line break\n * - `enter`: Enter submits, Shift+Enter adds a line break (Ctrl/Cmd+Enter submits too)\n */\nexport type ChatInputSubmitKey = 'mod+enter' | 'enter';\n\nexport interface ChatInputProps extends FormHTMLAttributes<HTMLFormElement> {\n value: string;\n placeholder?: string;\n onValueChange: (value: string) => void;\n onSubmit: () => void;\n /**\n * Keyboard shortcut that submits the message.\n * @default 'mod+enter'\n */\n submitKey?: ChatInputSubmitKey;\n /** While `true`, submitting is disabled and the send button becomes a stop button */\n isStreaming?: boolean;\n /**\n * Enables the microphone button. The browser speech recognition is used when available, this\n * callback is the fallback for when the browser does not support the Web Speech API.\n */\n onAudioClick?: () => void;\n /** Called when the stop button, shown in place of the send button while `isStreaming`, is clicked */\n onStop?: () => void;\n}\n\nfunction isSubmitKey(e: KeyboardEvent<HTMLTextAreaElement>, submitKey: ChatInputSubmitKey) {\n // Enter validates the text being composed with an IME: it must not submit\n if (e.key !== 'Enter' || e.nativeEvent.isComposing) return false;\n if (e.metaKey || e.ctrlKey) return true;\n\n return submitKey === 'enter' && !e.shiftKey;\n}\n\nconst FORM_PAD = 12; // matches p-12\nconst BTN_SIZE = 48; // matches size-48\nconst BTN_GAP = 8; // matches gap-8\n\n// Keep onValueChange stable inside the recognition handler without re-creating the SR instance\nfunction useCallbackRef<T extends (...args: never[]) => unknown>(fn: T) {\n const ref = useRef(fn);\n useEffect(() => {\n ref.current = fn;\n });\n return ref;\n}\n\nconst SILENCE_TIMEOUT_MS = 3_000;\n\nfunction useSpeechRecognition({\n enabled,\n onValueChange,\n}: {\n enabled: boolean;\n onValueChange: (v: string) => void;\n}) {\n const [isSupported, setIsSupported] = useState(false);\n const [isListening, setIsListening] = useState(false);\n const recognitionRef = useRef<SpeechRecognition | null>(null);\n const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const isListeningRef = useRef(false);\n const onValueChangeRef = useCallbackRef(onValueChange);\n\n function clearSilenceTimer() {\n if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);\n }\n\n function resetSilenceTimer() {\n clearSilenceTimer();\n silenceTimerRef.current = setTimeout(() => {\n isListeningRef.current = false;\n setIsListening(false);\n recognitionRef.current?.stop();\n }, SILENCE_TIMEOUT_MS);\n }\n\n useEffect(() => {\n const SR: typeof SpeechRecognition | undefined =\n (window as Window & { webkitSpeechRecognition?: typeof SpeechRecognition })\n .webkitSpeechRecognition ?? window.SpeechRecognition;\n if (!enabled || !SR) {\n return;\n }\n\n setIsSupported(true);\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = navigator.language;\n\n recognition.onresult = (event: SpeechRecognitionEvent) => {\n const transcript = Array.from(event.results)\n .map((r: SpeechRecognitionResult) => r[0].transcript)\n .join('');\n onValueChangeRef.current(transcript);\n resetSilenceTimer();\n };\n\n recognition.onend = () => {\n clearSilenceTimer();\n // Restart automatically if the user hasn't manually stopped (browser cut us off)\n if (isListeningRef.current) {\n try {\n recognition.start();\n } catch {\n /* already starting */\n }\n } else {\n setIsListening(false);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (e.error === 'not-allowed' || e.error === 'aborted') {\n isListeningRef.current = false;\n setIsListening(false);\n clearSilenceTimer();\n }\n };\n\n recognitionRef.current = recognition;\n return () => {\n recognition.abort();\n clearSilenceTimer();\n };\n // SR detection and instance creation only needs to run when the capability changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled]);\n\n function toggle() {\n const recognition = recognitionRef.current;\n if (!recognition) return;\n if (isListeningRef.current) {\n isListeningRef.current = false;\n recognition.stop();\n } else {\n isListeningRef.current = true;\n setIsListening(true);\n recognition.start();\n resetSilenceTimer();\n }\n }\n\n return { isSupported, isListening, toggle };\n}\n\n/**\n * Drives the audio-level indicator by writing directly to the DOM element's\n * style — avoids triggering a React re-render on every animation frame\n * (rule: rerender-use-ref-transient-values).\n *\n * Returns a ref to attach to the indicator <span>.\n */\nfunction useAudioLevel(isListening: boolean) {\n const indicatorRef = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n const el = indicatorRef.current;\n\n if (!isListening) {\n if (el) el.style.height = '0px';\n return;\n }\n\n let animId: number;\n let stream: MediaStream | null = null;\n let ctx: AudioContext | null = null;\n\n navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((s) => {\n stream = s;\n ctx = new AudioContext();\n const source = ctx.createMediaStreamSource(stream);\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n source.connect(analyser);\n const data = new Uint8Array(analyser.frequencyBinCount);\n\n function tick() {\n analyser.getByteFrequencyData(data);\n const avg = data.reduce((sum, v) => sum + v, 0) / data.length;\n // Square-root curve so quiet sounds register more visibly\n const node = indicatorRef.current;\n if (node) node.style.height = `${Math.sqrt(avg / 255) * 10}px`;\n animId = requestAnimationFrame(tick);\n }\n\n animId = requestAnimationFrame(tick);\n });\n\n return () => {\n cancelAnimationFrame(animId);\n stream?.getTracks().forEach((t) => t.stop());\n ctx?.close();\n };\n }, [isListening]);\n\n return indicatorRef;\n}\n\nexport function ChatInput({\n value,\n placeholder = 'Ask a question?',\n onValueChange,\n onSubmit,\n onAudioClick,\n onStop,\n isStreaming,\n submitKey = 'mod+enter',\n className,\n ...props\n}: ChatInputProps) {\n const { isSupported, isListening, toggle } = useSpeechRecognition({\n enabled: !!onAudioClick,\n onValueChange: onValueChange,\n });\n const audioIndicatorRef = useAudioLevel(isListening);\n const [isFocused, setIsFocused] = useState(false);\n const trimmed = value.trim();\n const isOpen = isFocused || trimmed.length > 0;\n const btnW = onAudioClick ? BTN_SIZE + BTN_GAP + BTN_SIZE : BTN_SIZE;\n\n function handleAudioClick() {\n if (isSupported) {\n toggle();\n } else {\n onAudioClick?.();\n }\n }\n\n function handleSubmit(e: SubmitEvent) {\n e.preventDefault();\n if (trimmed && !isStreaming) onSubmit();\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {\n if (!isSubmitKey(e, submitKey)) return;\n\n e.preventDefault();\n if (trimmed && !isStreaming) onSubmit();\n }\n\n function onMouseDown(e: MouseEvent) {\n if ((e.target as HTMLElement).tagName !== 'TEXTAREA') {\n e.preventDefault();\n }\n }\n\n return (\n <form\n onSubmit={handleSubmit}\n onMouseDown={onMouseDown}\n {...props}\n style={{\n paddingBottom: isOpen ? FORM_PAD + BTN_GAP + BTN_SIZE : FORM_PAD,\n }}\n className={twMerge(\n 'relative flex items-start overflow-hidden bg-white border border-lightGrey p-12 w-full',\n 'transition-[padding-bottom,border-radius] duration-250 ease-out rounded-b-[24px]',\n isOpen ? 'rounded-32' : 'rounded-[40px]',\n className,\n )}\n >\n <textarea\n rows={isOpen ? 3 : 1}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={onKeyDown}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n placeholder={placeholder}\n aria-label={placeholder}\n style={{\n marginInlineEnd: isOpen ? 0 : btnW + BTN_GAP,\n }}\n className={twMerge(\n 'scrollbar-hidden bg-transparent text-b3 py-12 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 resize-none w-full px-20 border border-middleGrey',\n 'transition-[margin-inline-end,border-radius] duration-250 ease-out',\n isOpen\n ? 'text-black rounded-[20px]'\n : 'text-darkGrey placeholder:text-darkGrey rounded-[40px]',\n )}\n />\n\n <div\n style={{\n insetInlineStart: isOpen\n ? `calc(50% - ${btnW / 2}px)`\n : `calc(100% - ${FORM_PAD + btnW}px)`,\n bottom: FORM_PAD,\n }}\n className=\"absolute flex gap-8 items-center transition-[inset-inline-start] duration-250 ease-out\"\n >\n {isStreaming ? (\n <Button\n type=\"button\"\n aria-label=\"Stop generating\"\n onClick={onStop}\n variant=\"circle\"\n color=\"black\"\n icon=\"CrossDefault\"\n iconWidth=\"24px\"\n />\n ) : (\n <Button\n type=\"submit\"\n aria-label=\"Send message\"\n disabled={!trimmed}\n variant=\"circle\"\n color=\"saffron\"\n icon=\"ArrowTailRight\"\n iconWidth=\"24px\"\n />\n )}\n\n {!!onAudioClick && (\n <Button\n type=\"button\"\n aria-label={isListening ? 'Stop recording' : 'Use microphone'}\n aria-pressed={isListening}\n onClick={handleAudioClick}\n variant=\"circle\"\n theme={isListening ? 'solid' : 'outline'}\n color={isListening ? 'sienna' : 'black'}\n icon=\"Audio\"\n iconWidth=\"24px\"\n className={twMerge(\n 'relative',\n isListening && 'animate-pulse',\n !isListening && 'border-lightGrey',\n )}\n >\n <span\n ref={audioIndicatorRef}\n aria-hidden=\"true\"\n className=\"bg-current absolute w-5 bottom-21\"\n style={{ height: '0px' }}\n />\n </Button>\n )}\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { HTMLAttributes, PropsWithChildren } from 'react';
2
+ import { ChatMessageLabels } from './ChatMessageActions';
3
+ export type ChatMessageSender = 'assistant' | 'user';
4
+ export interface ChatMessageProps extends HTMLAttributes<HTMLDivElement> {
5
+ sender: ChatMessageSender;
6
+ /** Actions of a user message, shown when it is hovered or focused. Ignored for assistant messages. */
7
+ onReplay?: () => void;
8
+ /**
9
+ * Enables the in-place edition of a text message: the bubble turns into an editor, and this is
10
+ * called with the new text when the user sends it. Ignored when the content is not a string.
11
+ */
12
+ onEdit?: (content: string) => void;
13
+ /** May be async: a confirmation is shown once the returned promise is resolved */
14
+ onCopy?: () => void | Promise<void>;
15
+ labels?: Partial<ChatMessageLabels>;
16
+ }
17
+ export declare const ChatMessage: import('react').NamedExoticComponent<PropsWithChildren<ChatMessageProps>>;
@@ -0,0 +1,2 @@
1
+ import { t as e } from "../../chunks/ChatMessage.js";
2
+ export { e as ChatMessage };