ss-support-widget 1.0.8 → 1.0.10

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.
@@ -0,0 +1,488 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import {
3
+ Avatar,
4
+ Badge,
5
+ Box,
6
+ Button,
7
+ Chip,
8
+ IconButton,
9
+ Paper,
10
+ TextField,
11
+ Typography,
12
+ } from "@mui/material";
13
+ import { useTheme, useMediaQuery } from "@mui/material";
14
+ import CloseIcon from "@mui/icons-material/Close";
15
+ import SendRoundedIcon from "@mui/icons-material/SendRounded";
16
+ import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded";
17
+ import SmartToyRoundedIcon from "@mui/icons-material/SmartToyRounded";
18
+ import PersonRoundedIcon from "@mui/icons-material/PersonRounded";
19
+ import ReactMarkdown from "react-markdown";
20
+ import { useViewportHeightVar } from "../hooks/hooks";
21
+ import { characterLimit } from "../constants";
22
+ import { Msg } from "../types";
23
+ import { ConversationMode } from "../containers/App";
24
+
25
+ export function ChatWidget({
26
+ open,
27
+ setOpen,
28
+ msgs,
29
+ input,
30
+ setInput,
31
+ sending,
32
+ send,
33
+ anchorSx,
34
+ conversationMode,
35
+ }: {
36
+ open: boolean;
37
+ setOpen: (v: boolean) => void;
38
+ msgs: Msg[];
39
+ input: string;
40
+ setInput: (v: string) => void;
41
+ sending: boolean;
42
+ send: () => void;
43
+ anchorSx?: any;
44
+ conversationMode: ConversationMode;
45
+ }) {
46
+
47
+ const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
48
+ const endRef = useRef<HTMLDivElement | null>(null);
49
+ const theme = useTheme();
50
+ const isMobile = useMediaQuery(theme.breakpoints.down("md"));
51
+ useViewportHeightVar();
52
+
53
+ useEffect(() => {
54
+ endRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
55
+ requestAnimationFrame(() => inputRef.current?.focus());
56
+ }, [msgs.length, sending]);
57
+
58
+ useEffect(() => {
59
+ if (!open) return;
60
+ endRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
61
+ }, [open]);
62
+
63
+ useEffect(() => {
64
+ if (!(open && isMobile)) return;
65
+
66
+ const scrollY = window.scrollY;
67
+
68
+ document.body.style.position = "fixed";
69
+ document.body.style.top = `-${scrollY}px`;
70
+ document.body.style.left = "0";
71
+ document.body.style.right = "0";
72
+ document.body.style.width = "100%";
73
+
74
+ return () => {
75
+ document.body.style.position = "";
76
+ document.body.style.top = "";
77
+ document.body.style.left = "";
78
+ document.body.style.right = "";
79
+ document.body.style.width = "";
80
+ window.scrollTo(0, scrollY);
81
+ };
82
+ }, [open, isMobile]);
83
+
84
+ const isMessageSeen = useCallback(
85
+ (idx: number, isMe: boolean, messages: Msg[], m: Msg) => {
86
+ const isLast = idx === messages.length - 1;
87
+ const lastSeenMessage = messages[messages.length - 1];
88
+
89
+ const showSeenIndicator = isLast && isMe && lastSeenMessage?.seen;
90
+ const isSeen = showSeenIndicator && lastSeenMessage?.id === m.id;
91
+
92
+ return { showSeenIndicator, isSeen };
93
+ },
94
+ []
95
+ );
96
+
97
+ const getConversationModeLabel = (mode: ConversationMode): string => {
98
+ if (mode === "AI") return "Răspunde automat"
99
+ if (mode === "Human") return "In conversatie cu un operator"
100
+ if (mode === "Disconnected") return "Conexiune pierduta. Reincarcati pagina!"
101
+ if (mode === "Ended") return "Conversație închisă"
102
+ return "Conversație nouă"
103
+ }
104
+
105
+ return (
106
+ <Box sx={anchorSx}>
107
+ {!open && (
108
+ <Button
109
+ variant="contained"
110
+ onClick={() => setOpen(true)}
111
+ startIcon={<ChatBubbleOutlineRoundedIcon />}
112
+ sx={{
113
+ borderRadius: 999,
114
+ px: 2,
115
+ py: 1,
116
+ textTransform: "none",
117
+ boxShadow: "0 10px 30px rgba(0,0,0,.18)",
118
+ }}
119
+ >
120
+ Chat
121
+ </Button>
122
+ )}
123
+
124
+ {open && (
125
+ <Paper
126
+ elevation={isMobile ? 0 : 10}
127
+ sx={{
128
+ width: isMobile ? "100vw" : 380,
129
+ height: isMobile ? "calc(var(--vh, 1vh) * 100)" : 560,
130
+ "@supports (height: 100dvh)": {
131
+ height: isMobile ? "100dvh" : 560,
132
+ },
133
+ maxWidth: "100vw",
134
+ maxHeight: "100dvh",
135
+ borderRadius: isMobile ? 0 : 4,
136
+ position: isMobile ? "fixed" : "relative",
137
+ inset: isMobile ? 0 : "auto",
138
+ overflow: "hidden",
139
+ border: isMobile ? "none" : "1px solid",
140
+ borderColor: "divider",
141
+ boxShadow: isMobile ? "none" : "0 18px 60px rgba(0,0,0,.28)",
142
+ }}
143
+ >
144
+ <Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
145
+
146
+ <Box
147
+ sx={{
148
+ px: 1.75,
149
+ py: 1.25,
150
+ display: "flex",
151
+ alignItems: "center",
152
+ gap: 1,
153
+ borderBottom: "1px solid",
154
+ borderColor: "divider",
155
+ background:
156
+ "linear-gradient(135deg, rgba(25,118,210,.18), rgba(156,39,176,.14))",
157
+ backdropFilter: isMobile ? "none" : "blur(10px)",
158
+ }}
159
+ >
160
+ <Badge
161
+ overlap="circular"
162
+ variant="dot"
163
+ color={conversationMode != "Disconnected" ? "success" : "error"}
164
+ sx={{
165
+ "& .MuiBadge-badge": {
166
+ width: 10,
167
+ height: 10,
168
+ borderRadius: 999,
169
+ border: "2px solid",
170
+ borderColor: "background.paper",
171
+ },
172
+ }}
173
+ >
174
+ <Avatar sx={{ bgcolor: "primary.main" }}>
175
+ <SmartToyRoundedIcon />
176
+ </Avatar>
177
+ </Badge>
178
+
179
+ <Box sx={{ flex: 1, minWidth: 0 }}>
180
+ <Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.1 }}>
181
+ Asistent
182
+ </Typography>
183
+ <Typography variant="caption" sx={{ color: "text.secondary" }}>
184
+ {getConversationModeLabel(conversationMode)}
185
+ </Typography>
186
+ </Box>
187
+
188
+ <IconButton
189
+ size="small"
190
+ onClick={() => setOpen(false)}
191
+ sx={{
192
+ bgcolor: "rgba(255,255,255,.55)",
193
+ border: "1px solid",
194
+ borderColor: "divider",
195
+ "&:hover": { bgcolor: "rgba(255,255,255,.75)" },
196
+ }}
197
+ >
198
+ <CloseIcon fontSize="small" />
199
+ </IconButton>
200
+ </Box>
201
+
202
+ <Box
203
+ sx={{
204
+ flex: 1,
205
+ minHeight: 0,
206
+ overflow: "auto",
207
+ px: 1.5,
208
+ py: 1.5,
209
+ backgroundImage:
210
+ "radial-gradient(circle at 20% 10%, rgba(25,118,210,.10), transparent 45%)," +
211
+ "radial-gradient(circle at 80% 20%, rgba(156,39,176,.10), transparent 50%)," +
212
+ "radial-gradient(circle at 40% 90%, rgba(0,200,83,.08), transparent 45%)",
213
+ "&::-webkit-scrollbar": { width: 10 },
214
+ "&::-webkit-scrollbar-thumb": {
215
+ backgroundColor: "rgba(0,0,0,.18)",
216
+ borderRadius: 999,
217
+ border: "3px solid transparent",
218
+ backgroundClip: "content-box",
219
+ },
220
+ }}
221
+ >
222
+ <MessageList msgs={msgs} isMessageSeen={isMessageSeen} />
223
+ <div ref={endRef} style={{ paddingBottom: 2 }}>
224
+ {sending && (
225
+ <Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, mb: 1.2 }}>
226
+ <Avatar sx={{ width: 30, height: 30, bgcolor: "primary.main" }}>
227
+ <SmartToyRoundedIcon sx={{ fontSize: 18 }} />
228
+ </Avatar>
229
+
230
+ <Box
231
+ sx={{
232
+ px: 1.4,
233
+ py: 1.1,
234
+ borderRadius: 3,
235
+ border: "1px solid",
236
+ borderColor: "divider",
237
+ bgcolor: "background.paper",
238
+ boxShadow: "0 10px 30px rgba(0,0,0,.10)",
239
+ display: "flex",
240
+ alignItems: "center",
241
+ gap: 0.6,
242
+ }}
243
+ >
244
+ <Box
245
+ sx={{
246
+ width: 6,
247
+ height: 6,
248
+ borderRadius: 999,
249
+ bgcolor: "text.secondary",
250
+ animation: "dot 1.1s infinite ease-in-out",
251
+ "@keyframes dot": {
252
+ "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
253
+ "40%": { transform: "scale(1)", opacity: 1 },
254
+ },
255
+ }}
256
+ />
257
+ <Box
258
+ sx={{
259
+ width: 6,
260
+ height: 6,
261
+ borderRadius: 999,
262
+ bgcolor: "text.secondary",
263
+ animation: "dot 1.1s infinite ease-in-out",
264
+ animationDelay: "0.15s",
265
+ "@keyframes dot": {
266
+ "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
267
+ "40%": { transform: "scale(1)", opacity: 1 },
268
+ },
269
+ }}
270
+ />
271
+ <Box
272
+ sx={{
273
+ width: 6,
274
+ height: 6,
275
+ borderRadius: 999,
276
+ bgcolor: "text.secondary",
277
+ animation: "dot 1.1s infinite ease-in-out",
278
+ animationDelay: "0.3s",
279
+ "@keyframes dot": {
280
+ "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
281
+ "40%": { transform: "scale(1)", opacity: 1 },
282
+ },
283
+ }}
284
+ />
285
+ </Box>
286
+ </Box>
287
+ )}
288
+ </div>
289
+ </Box>
290
+
291
+ <Box
292
+ sx={{
293
+ p: 1.25,
294
+ borderTop: "1px solid",
295
+ borderColor: "divider",
296
+ display: "flex",
297
+ gap: 1,
298
+ bgcolor: "background.paper",
299
+ flexShrink: 0,
300
+ }}
301
+ >
302
+ <TextField
303
+ inputRef={inputRef}
304
+ focused
305
+ size="small"
306
+ fullWidth
307
+ value={input}
308
+ disabled={sending || input.length >= characterLimit}
309
+ onChange={(e) => setInput(e.target.value)}
310
+ onKeyDown={(e) => {
311
+ if (e.key === "Enter" && !e.shiftKey) {
312
+ e.preventDefault();
313
+ send();
314
+ }
315
+ }}
316
+ placeholder="Scrie un mesaj"
317
+ multiline
318
+ maxRows={4}
319
+ sx={{
320
+ "& .MuiOutlinedInput-root": {
321
+ borderRadius: 3,
322
+ pr: 0.75,
323
+ backgroundColor: "rgba(0,0,0,.02)",
324
+ },
325
+ }}
326
+ InputProps={{
327
+ endAdornment: (
328
+ <IconButton
329
+ size="small"
330
+ onClick={send}
331
+ disabled={sending || !input.trim()}
332
+ sx={{
333
+ ml: 0.5,
334
+ borderRadius: 999,
335
+ }}
336
+ >
337
+ <SendRoundedIcon />
338
+ </IconButton>
339
+ ),
340
+ }}
341
+ />
342
+ </Box>
343
+
344
+ </Box>
345
+ </Paper>
346
+ )}
347
+ </Box>
348
+ );
349
+ }
350
+ export default ChatWidget;
351
+
352
+
353
+ type MessageListProps = {
354
+ msgs: Msg[];
355
+ isMessageSeen: (idx: number, isMe: boolean, msgs: Msg[], msg: Msg) => { showSeenIndicator: boolean; isSeen: boolean };
356
+ endRef?: React.RefObject<HTMLDivElement | null> | null;
357
+ };
358
+
359
+ const MessageList = React.memo(function MessageList({ msgs, isMessageSeen, endRef }: MessageListProps) {
360
+ return <>
361
+ {msgs.map((m, idx) => {
362
+ const isMe = m.role === "User";
363
+ const { showSeenIndicator, isSeen } = isMessageSeen(idx, isMe, msgs, m);
364
+
365
+ return (
366
+ <Box
367
+ key={m.id}
368
+ sx={{
369
+ display: "flex",
370
+ justifyContent: isMe ? "flex-end" : "flex-start",
371
+ gap: 1,
372
+ mb: 1.2,
373
+ overflowAnchor: "none",
374
+ }}
375
+ >
376
+ {!isMe && (
377
+ <Box key={"asistent" + m.id}>
378
+ <Avatar
379
+ sx={{
380
+ width: 30,
381
+ height: 30,
382
+ mt: 0.25,
383
+ bgcolor: "primary.main",
384
+ }}
385
+ >
386
+ <SmartToyRoundedIcon sx={{ fontSize: 18 }} />
387
+ </Avatar>
388
+ {m.sentAt && (
389
+ <Box sx={{ display: "flex", justifyContent: isMe ? "flex-end" : "flex-start", mt: 0.5 }}>
390
+ <Typography variant="caption" sx={{ color: "text.secondary", fontSize: 11 }}>
391
+ {m.sentAt}
392
+ </Typography>
393
+ </Box>
394
+ )}
395
+ </Box>
396
+ )}
397
+
398
+ <Box sx={{ maxWidth: "78%" }} key={"container" + m.id}>
399
+ <Box
400
+ sx={{
401
+ wordBreak: "break-word",
402
+ position: "relative",
403
+ px: 1.4,
404
+ py: 1.05,
405
+ borderRadius: 3,
406
+ border: "1px solid",
407
+ borderColor: "divider",
408
+ bgcolor: isMe ? "primary.main" : "background.paper",
409
+ color: isMe ? "primary.contrastText" : "text.primary",
410
+ boxShadow: isMe
411
+ ? "0 10px 30px rgba(25,118,210,.22)"
412
+ : "0 10px 30px rgba(0,0,0,.10)",
413
+ whiteSpace: "pre-wrap",
414
+ "&:before": {
415
+ content: '""',
416
+ position: "absolute",
417
+ top: 14,
418
+ width: 10,
419
+ height: 10,
420
+ transform: "rotate(45deg)",
421
+ bgcolor: isMe ? "primary.main" : "background.paper",
422
+ borderLeft: isMe ? "none" : "1px solid",
423
+ borderTop: isMe ? "none" : "1px solid",
424
+ borderColor: "divider",
425
+ right: isMe ? -5 : "auto",
426
+ left: isMe ? "auto" : -5,
427
+ },
428
+ "& p": { margin: 0 },
429
+ "& a": { color: "inherit", textDecoration: "underline" },
430
+ "& code": {
431
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
432
+ fontSize: "0.9em",
433
+ padding: "2px 6px",
434
+ borderRadius: 8,
435
+ backgroundColor: isMe ? "rgba(255,255,255,.14)" : "rgba(0,0,0,.06)",
436
+ },
437
+ "& pre": {
438
+ margin: 0,
439
+ marginTop: 8,
440
+ padding: 12,
441
+ overflow: "auto",
442
+ borderRadius: 12,
443
+ backgroundColor: isMe ? "rgba(255,255,255,.12)" : "rgba(0,0,0,.05)",
444
+ },
445
+ }}
446
+ >
447
+ <ReactMarkdown key={"message" + m.id}>{m.text}</ReactMarkdown>
448
+
449
+ </Box>
450
+
451
+ {showSeenIndicator ? (
452
+ <Typography sx={{ mt: 0.35, fontSize: 12, textAlign: "right", color: isSeen ? "text.secondary" : "text.disabled" }}>
453
+ {isSeen ? "Seen" : "Sent"}
454
+ </Typography>
455
+ ) : null}
456
+ </Box>
457
+ <Box>
458
+
459
+ {isMe && (
460
+ <Box key={"sentad" + m.id}>
461
+ <Avatar
462
+ sx={{
463
+ width: 30,
464
+ height: 30,
465
+ mt: 0.25,
466
+ bgcolor: "grey.900",
467
+ }}
468
+ >
469
+ <PersonRoundedIcon sx={{ fontSize: 18 }} />
470
+ </Avatar>
471
+ {m.sentAt && (
472
+ <Box sx={{ display: "flex", justifyContent: isMe ? "flex-end" : "flex-start", mt: 0.5 }}>
473
+ <Typography variant="caption" sx={{ color: "text.secondary", fontSize: 11 }}>
474
+ {m.sentAt}
475
+ </Typography>
476
+ </Box>
477
+ )}
478
+ </Box>
479
+
480
+ )}
481
+
482
+ </Box>
483
+ </Box>
484
+ );
485
+ })}
486
+ <div ref={endRef} />
487
+ </>;
488
+ });
@@ -0,0 +1,53 @@
1
+ import * as client from "react-dom/client";
2
+ import createCache from "@emotion/cache";
3
+ import { CacheProvider } from "@emotion/react";
4
+ import { CssBaseline, ThemeProvider, createTheme } from "@mui/material";
5
+ import App from "../containers/App";
6
+ import { ensureViewportMeta } from "../hooks/hooks";
7
+ import { getChatBotConfig } from "../services/chatConfiguration";
8
+
9
+
10
+ export class ChatBotElement extends HTMLElement {
11
+ private root?: client.Root;
12
+ private mountEl?: HTMLDivElement;
13
+
14
+ connectedCallback() {
15
+ const chatConfiguration = getChatBotConfig()
16
+ if (!chatConfiguration) return;
17
+
18
+ const shadow = this.attachShadow({ mode: "open" });
19
+
20
+ const styleHost = document.createElement("style");
21
+ styleHost.textContent = `
22
+ :host { all: initial; }
23
+ `;
24
+ shadow.appendChild(styleHost);
25
+
26
+ this.mountEl = document.createElement("div");
27
+ shadow.appendChild(this.mountEl);
28
+
29
+ ensureViewportMeta();
30
+
31
+ const cache = createCache({
32
+ key: "chatbot",
33
+ container: shadow as unknown as HTMLElement,
34
+ });
35
+
36
+ const theme = createTheme();
37
+
38
+ this.root = client.createRoot(this.mountEl);
39
+ this.root.render(
40
+ <CacheProvider value={cache}>
41
+ <ThemeProvider theme={theme}>
42
+ <CssBaseline />
43
+ <App config={chatConfiguration} />
44
+ </ThemeProvider>
45
+ </CacheProvider>
46
+ );
47
+ }
48
+
49
+ disconnectedCallback() {
50
+ this.root?.unmount();
51
+ this.root = undefined;
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ export const characterLimit = 2000;
2
+ export const chatBasePath = "/api/chat";
3
+ export const sessionBasePath = "/api/chat-session";