ss-support-widget 1.0.9 → 1.0.11

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/src/App.tsx DELETED
@@ -1,106 +0,0 @@
1
- // App.tsx
2
- import React, { useEffect, useMemo, useState } from "react";
3
- import { Box, Button, IconButton, Paper, TextField, Typography } from "@mui/material";
4
- import CloseIcon from "@mui/icons-material/Close";
5
- import { streamChat } from "./sse";
6
- import { Config } from "./element";
7
- import { MsgDelta } from "./MsgDelta";
8
- import ReactMarkdown from "react-markdown";
9
- import { getHistoryMessages } from "./service";
10
- import { getThreadId, saveThreadId } from "./session-storage";
11
- import ChatWidget from "./ChatWidget";
12
-
13
- export type Msg = { id: string; role: string | "user" | "assistant"; text: string, timestamp?: string; };
14
- export default function App({ config }: { config: Config }) {
15
- const [open, setOpen] = useState(false);
16
- const [input, setInput] = useState("");
17
- const [threadId, setThreadId] = useState(getThreadId() || config.threadId || "");
18
- const [msgs, setMsgs] = useState<Msg[]>([]);
19
- const [sending, setSending] = useState(false);
20
- const [hideChat, setHideChat] = useState(false);
21
-
22
- const anchorSx = useMemo(
23
- () => ({
24
- position: "fixed",
25
- right: 16,
26
- bottom: 16,
27
- zIndex: 2147483647,
28
- fontFamily: "system-ui",
29
- }),
30
- []
31
- );
32
-
33
- useEffect(() => {
34
- const path = window.location.pathname;
35
- if (config.hideChatForUrls?.some(u => path.startsWith(u))) {
36
- setHideChat(true);
37
- } else {
38
- setHideChat(false);
39
- }
40
- });
41
-
42
- useEffect(() => {
43
- getHistoryMessages(config,
44
- getThreadId() || config.threadId || "",
45
- (delta) => {
46
- const msgs = delta.map((m: MsgDelta) => ({ id: m.id, role: m.role, text: m.text }))
47
- setMsgs((prev) => [...prev, ...msgs]);
48
- });
49
- }, []);
50
-
51
- function upsertAssistantDelta(delta: MsgDelta) {
52
- setMsgs((prev) => {
53
- const last = prev[prev.length - 1];
54
- if (!last || last.role !== "assistant" || last.id !== delta.id) {
55
- return [...prev, { id: delta.id, role: delta.role, text: delta.text }];
56
- }
57
- const updated = prev.slice();
58
- updated[updated.length - 1] = { ...last, text: last.text + delta.text };
59
- return updated;
60
- });
61
-
62
-
63
- }
64
-
65
- async function send() {
66
- const text = input.trim();
67
- if (!text || sending) return;
68
-
69
- setSending(true);
70
- setInput("");
71
- setMsgs((m) => [...m, { id: crypto.randomUUID(), role: "user", text }]);
72
-
73
- try {
74
- await streamChat({
75
- url: config.apiBaseUrl + "api/chat-support",
76
- token: config.token,
77
- clientId: config.clientId,
78
- threadId: threadId,
79
- body: text,
80
- onDelta: (delta) => {
81
- upsertAssistantDelta(delta)
82
- setThreadId(delta.threadId);
83
- saveThreadId(delta.threadId);
84
- },
85
- });
86
- } catch {
87
- setMsgs((m) => [...m, { id: crypto.randomUUID(), role: "assistant", text: "Eroare la trimitere." }]);
88
- } finally {
89
- setSending(false);
90
- }
91
- }
92
-
93
- return (
94
- hideChat
95
- ? <></>
96
- : <ChatWidget
97
- anchorSx={anchorSx}
98
- open={open}
99
- setOpen={setOpen}
100
- msgs={msgs}
101
- input={input}
102
- setInput={setInput}
103
- send={send}
104
- sending={sending} />
105
- );
106
- }
@@ -1,428 +0,0 @@
1
- import React, { useEffect, 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 { Msg } from "./App";
21
- import { useViewportHeightVar } from "./hooks";
22
-
23
-
24
- export function ChatWidget({
25
- open,
26
- setOpen,
27
- msgs,
28
- input,
29
- setInput,
30
- sending,
31
- send,
32
- anchorSx,
33
- }: {
34
- open: boolean;
35
- setOpen: (v: boolean) => void;
36
- msgs: Msg[];
37
- input: string;
38
- setInput: (v: string) => void;
39
- sending: boolean;
40
- send: () => void;
41
- anchorSx?: any;
42
- }) {
43
- const endRef = useRef<HTMLDivElement | null>(null);
44
- const theme = useTheme();
45
- const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
46
- useViewportHeightVar();
47
-
48
- useEffect(() => {
49
- endRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
50
- }, [msgs.length, sending]);
51
-
52
- useEffect(() => {
53
- if (!open) return;
54
- endRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
55
- }, [open]);
56
-
57
- useEffect(() => {
58
- if (!(open && isMobile)) return;
59
-
60
- const scrollY = window.scrollY;
61
-
62
- document.body.style.position = "fixed";
63
- document.body.style.top = `-${scrollY}px`;
64
- document.body.style.left = "0";
65
- document.body.style.right = "0";
66
- document.body.style.width = "100%";
67
-
68
- return () => {
69
- document.body.style.position = "";
70
- document.body.style.top = "";
71
- document.body.style.left = "";
72
- document.body.style.right = "";
73
- document.body.style.width = "";
74
- window.scrollTo(0, scrollY);
75
- };
76
- }, [open, isMobile]);
77
-
78
-
79
-
80
- return (
81
- <Box sx={anchorSx}>
82
- {!open && (
83
- <Button
84
- variant="contained"
85
- onClick={() => setOpen(true)}
86
- startIcon={<ChatBubbleOutlineRoundedIcon />}
87
- sx={{
88
- borderRadius: 999,
89
- px: 2,
90
- py: 1,
91
- textTransform: "none",
92
- boxShadow: "0 10px 30px rgba(0,0,0,.18)",
93
- }}
94
- >
95
- Chat
96
- </Button>
97
- )}
98
-
99
- {open && (
100
- <Paper
101
- elevation={isMobile ? 0 : 10}
102
- sx={{
103
- width: isMobile ? "100vw" : 380,
104
- height: isMobile ? "calc(var(--vh, 1vh) * 100)" : 560,
105
- "@supports (height: 100dvh)": {
106
- height: isMobile ? "100dvh" : 560,
107
- },
108
- maxWidth: "100vw",
109
- maxHeight: "100dvh",
110
- borderRadius: isMobile ? 0 : 4,
111
- position: isMobile ? "fixed" : "relative",
112
- inset: isMobile ? 0 : "auto",
113
- overflow: "hidden",
114
- border: isMobile ? "none" : "1px solid",
115
- borderColor: "divider",
116
- boxShadow: isMobile ? "none" : "0 18px 60px rgba(0,0,0,.28)",
117
- }}
118
- >
119
- <Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
120
-
121
- <Box
122
- sx={{
123
- px: 1.75,
124
- py: 1.25,
125
- display: "flex",
126
- alignItems: "center",
127
- gap: 1,
128
- borderBottom: "1px solid",
129
- borderColor: "divider",
130
- background:
131
- "linear-gradient(135deg, rgba(25,118,210,.18), rgba(156,39,176,.14))",
132
- backdropFilter: isMobile ? "none" : "blur(10px)",
133
- }}
134
- >
135
- <Badge
136
- overlap="circular"
137
- variant="dot"
138
- color="success"
139
- sx={{
140
- "& .MuiBadge-badge": {
141
- width: 10,
142
- height: 10,
143
- borderRadius: 999,
144
- border: "2px solid",
145
- borderColor: "background.paper",
146
- },
147
- }}
148
- >
149
- <Avatar sx={{ bgcolor: "primary.main" }}>
150
- <SmartToyRoundedIcon />
151
- </Avatar>
152
- </Badge>
153
-
154
- <Box sx={{ flex: 1, minWidth: 0 }}>
155
- <Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.1 }}>
156
- Asistent
157
- </Typography>
158
- <Typography variant="caption" sx={{ color: "text.secondary" }}>
159
- Online
160
- </Typography>
161
- </Box>
162
-
163
- <IconButton
164
- size="small"
165
- onClick={() => setOpen(false)}
166
- sx={{
167
- bgcolor: "rgba(255,255,255,.55)",
168
- border: "1px solid",
169
- borderColor: "divider",
170
- "&:hover": { bgcolor: "rgba(255,255,255,.75)" },
171
- }}
172
- >
173
- <CloseIcon fontSize="small" />
174
- </IconButton>
175
- </Box>
176
-
177
- <Box
178
- sx={{
179
- flex: 1,
180
- minHeight: 0,
181
- position: "relative",
182
- // height: 300,
183
- overflow: "auto",
184
- px: 1.5,
185
- py: 1.5,
186
- bgcolor: "background.default",
187
- backgroundImage:
188
- "radial-gradient(circle at 20% 10%, rgba(25,118,210,.10), transparent 45%)," +
189
- "radial-gradient(circle at 80% 20%, rgba(156,39,176,.10), transparent 50%)," +
190
- "radial-gradient(circle at 40% 90%, rgba(0,200,83,.08), transparent 45%)",
191
- "&::-webkit-scrollbar": { width: 10 },
192
- "&::-webkit-scrollbar-thumb": {
193
- backgroundColor: "rgba(0,0,0,.18)",
194
- borderRadius: 999,
195
- border: "3px solid transparent",
196
- backgroundClip: "content-box",
197
- },
198
- overscrollBehavior: "contain",
199
- WebkitOverflowScrolling: "touch",
200
- }}
201
- >
202
- {msgs.map((m) => {
203
- const isUser = m.role === "user";
204
- return (
205
- <Box
206
- key={m.id}
207
- sx={{
208
- display: "flex",
209
- justifyContent: isUser ? "flex-end" : "flex-start",
210
- gap: 1,
211
- mb: 1.2,
212
- }}
213
- >
214
- {!isUser && (
215
- <Avatar
216
- sx={{
217
- width: 30,
218
- height: 30,
219
- mt: 0.25,
220
- bgcolor: "primary.main",
221
- }}
222
- >
223
- <SmartToyRoundedIcon sx={{ fontSize: 18 }} />
224
- </Avatar>
225
- )}
226
-
227
- <Box sx={{ maxWidth: "78%" }}>
228
- {m.timestamp && (
229
- <Box sx={{ display: "flex", justifyContent: isUser ? "flex-end" : "flex-start", mb: 0.5 }}>
230
- <Chip size="small" label={m.timestamp} variant="outlined" sx={{ height: 22 }} />
231
- </Box>
232
- )}
233
-
234
- <Box
235
- sx={{
236
- position: "relative",
237
- px: 1.4,
238
- py: 1.05,
239
- borderRadius: 3,
240
- border: "1px solid",
241
- borderColor: "divider",
242
- bgcolor: isUser ? "primary.main" : "background.paper",
243
- color: isUser ? "primary.contrastText" : "text.primary",
244
- boxShadow: isUser
245
- ? "0 10px 30px rgba(25,118,210,.22)"
246
- : "0 10px 30px rgba(0,0,0,.10)",
247
- whiteSpace: "pre-wrap",
248
- "&:before": {
249
- content: '""',
250
- position: "absolute",
251
- top: 14,
252
- width: 10,
253
- height: 10,
254
- transform: "rotate(45deg)",
255
- bgcolor: isUser ? "primary.main" : "background.paper",
256
- borderLeft: isUser ? "none" : "1px solid",
257
- borderTop: isUser ? "none" : "1px solid",
258
- borderColor: "divider",
259
- right: isUser ? -5 : "auto",
260
- left: isUser ? "auto" : -5,
261
- },
262
- "& p": { margin: 0 },
263
- "& a": { color: "inherit", textDecoration: "underline" },
264
- "& code": {
265
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
266
- fontSize: "0.9em",
267
- padding: "2px 6px",
268
- borderRadius: 8,
269
- backgroundColor: isUser ? "rgba(255,255,255,.14)" : "rgba(0,0,0,.06)",
270
- },
271
- "& pre": {
272
- margin: 0,
273
- marginTop: 8,
274
- padding: 12,
275
- overflow: "auto",
276
- borderRadius: 12,
277
- backgroundColor: isUser ? "rgba(255,255,255,.12)" : "rgba(0,0,0,.05)",
278
- },
279
- }}
280
- >
281
- <ReactMarkdown>{m.text}</ReactMarkdown>
282
- </Box>
283
- </Box>
284
-
285
- {isUser && (
286
- <Avatar
287
- sx={{
288
- width: 30,
289
- height: 30,
290
- mt: 0.25,
291
- bgcolor: "grey.900",
292
- }}
293
- >
294
- <PersonRoundedIcon sx={{ fontSize: 18 }} />
295
- </Avatar>
296
- )}
297
- </Box>
298
- );
299
- })}
300
-
301
- {sending && (
302
- <Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, mb: 1.2 }}>
303
- <Avatar sx={{ width: 30, height: 30, bgcolor: "primary.main" }}>
304
- <SmartToyRoundedIcon sx={{ fontSize: 18 }} />
305
- </Avatar>
306
-
307
- <Box
308
- sx={{
309
- px: 1.4,
310
- py: 1.1,
311
- borderRadius: 3,
312
- border: "1px solid",
313
- borderColor: "divider",
314
- bgcolor: "background.paper",
315
- boxShadow: "0 10px 30px rgba(0,0,0,.10)",
316
- display: "flex",
317
- alignItems: "center",
318
- gap: 0.6,
319
- }}
320
- >
321
- <Box
322
- sx={{
323
- width: 6,
324
- height: 6,
325
- borderRadius: 999,
326
- bgcolor: "text.secondary",
327
- animation: "dot 1.1s infinite ease-in-out",
328
- "@keyframes dot": {
329
- "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
330
- "40%": { transform: "scale(1)", opacity: 1 },
331
- },
332
- }}
333
- />
334
- <Box
335
- sx={{
336
- width: 6,
337
- height: 6,
338
- borderRadius: 999,
339
- bgcolor: "text.secondary",
340
- animation: "dot 1.1s infinite ease-in-out",
341
- animationDelay: "0.15s",
342
- "@keyframes dot": {
343
- "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
344
- "40%": { transform: "scale(1)", opacity: 1 },
345
- },
346
- }}
347
- />
348
- <Box
349
- sx={{
350
- width: 6,
351
- height: 6,
352
- borderRadius: 999,
353
- bgcolor: "text.secondary",
354
- animation: "dot 1.1s infinite ease-in-out",
355
- animationDelay: "0.3s",
356
- "@keyframes dot": {
357
- "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
358
- "40%": { transform: "scale(1)", opacity: 1 },
359
- },
360
- }}
361
- />
362
- </Box>
363
- </Box>
364
- )}
365
-
366
- <div ref={endRef} />
367
- </Box>
368
-
369
- <Box
370
- sx={{
371
- position: "sticky",
372
- bottom: 0,
373
- zIndex: 2,
374
- p: 1.25,
375
- borderTop: "1px solid",
376
- borderColor: "divider",
377
- display: "flex",
378
- gap: 1,
379
- bgcolor: "background.paper",
380
- }}
381
- >
382
- <TextField
383
- size="small"
384
- fullWidth
385
- value={input}
386
- disabled={sending}
387
- onChange={(e) => setInput(e.target.value)}
388
- onKeyDown={(e) => {
389
- if (e.key === "Enter" && !e.shiftKey) {
390
- e.preventDefault();
391
- send();
392
- }
393
- }}
394
- placeholder="Scrie un mesaj"
395
- multiline
396
- maxRows={4}
397
- sx={{
398
- "& .MuiOutlinedInput-root": {
399
- borderRadius: 999,
400
- pr: 0.75,
401
- backgroundColor: "rgba(0,0,0,.02)",
402
- },
403
- }}
404
- InputProps={{
405
- endAdornment: (
406
- <IconButton
407
- size="small"
408
- onClick={send}
409
- disabled={sending || !input.trim()}
410
- sx={{
411
- ml: 0.5,
412
- borderRadius: 999,
413
- }}
414
- >
415
- <SendRoundedIcon />
416
- </IconButton>
417
- ),
418
- }}
419
- />
420
- </Box>
421
-
422
- </Box>
423
- </Paper>
424
- )}
425
- </Box>
426
- );
427
- }
428
- export default ChatWidget;
package/src/MsgDelta.tsx DELETED
@@ -1,6 +0,0 @@
1
- export type MsgDelta = {
2
- id: string;
3
- threadId: string;
4
- text: string;
5
- role: string | "user" | "assistant";
6
- };
package/src/element.tsx DELETED
@@ -1,100 +0,0 @@
1
- import React from "react";
2
- import { createRoot, Root } from "react-dom/client";
3
- import createCache from "@emotion/cache";
4
- import { CacheProvider } from "@emotion/react";
5
- import { CssBaseline, ThemeProvider, createTheme } from "@mui/material";
6
- import App from "./App";
7
- import { ensureViewportMeta } from "./hooks";
8
- import { getApiStatus, getClientActivityStatus, getHideChatForUrls } from "./service";
9
-
10
- export type Config = {
11
- clientId: string;
12
- apiBaseUrl: string;
13
- threadId?: string;
14
- token?: string;
15
- userId?: string;
16
- hideChatForUrls?: string[];
17
- };
18
-
19
- const cfg = (window as any).ChatBotConfig;
20
- const chatBotComponentName = `chat-bot-${cfg.clientId}`;
21
-
22
- class ChatBotElement extends HTMLElement {
23
- private root?: Root;
24
- private mountEl?: HTMLDivElement;
25
-
26
- connectedCallback() {
27
- const cfg = this.readConfig();
28
- if (!cfg) return;
29
-
30
- const shadow = this.attachShadow({ mode: "open" });
31
-
32
- const styleHost = document.createElement("style");
33
- styleHost.textContent = `
34
- :host { all: initial; }
35
- `;
36
- shadow.appendChild(styleHost);
37
-
38
- this.mountEl = document.createElement("div");
39
- shadow.appendChild(this.mountEl);
40
-
41
- ensureViewportMeta();
42
-
43
- const cache = createCache({
44
- key: "chatbot",
45
- container: shadow as unknown as HTMLElement,
46
- });
47
-
48
- const theme = createTheme();
49
-
50
- this.root = createRoot(this.mountEl);
51
- this.root.render(
52
- <CacheProvider value={cache}>
53
- <ThemeProvider theme={theme}>
54
- <CssBaseline />
55
- <App config={cfg} />
56
- </ThemeProvider>
57
- </CacheProvider>
58
- );
59
- }
60
-
61
- disconnectedCallback() {
62
- this.root?.unmount();
63
- this.root = undefined;
64
- }
65
-
66
- private readConfig(): Config | null {
67
- const raw = (window as any).ChatBotConfig as Partial<Config> | undefined;
68
-
69
- const clientId = this.getAttribute("client-id") || raw?.clientId || "";
70
- const apiBaseUrl = this.getAttribute("api-base-url") || raw?.apiBaseUrl || "";
71
- const threadId = this.getAttribute("thread-id") || raw?.threadId || "";
72
-
73
- const token = this.getAttribute("token") || raw?.token || undefined;
74
- const userId = this.getAttribute("user-id") || raw?.userId || undefined;
75
-
76
- const hideChatForUrls = raw?.hideChatForUrls || []
77
-
78
- if (!clientId || !apiBaseUrl) return null;
79
- return { clientId, apiBaseUrl, threadId, token, userId, hideChatForUrls };
80
- }
81
- }
82
-
83
- if (!customElements.get(chatBotComponentName)) {
84
- customElements.define(chatBotComponentName, ChatBotElement);
85
- }
86
-
87
- (async function autoMount() {
88
- if (!cfg) return;
89
- if (document.querySelector(chatBotComponentName)) return;
90
-
91
- var isHealty = await getApiStatus(cfg)
92
- if (!isHealty) return;
93
-
94
- var isClientActive = await getClientActivityStatus(cfg)
95
- if (!isClientActive) return;
96
-
97
- cfg.hideChatForUrls = await getHideChatForUrls(cfg)
98
- const el = document.createElement(chatBotComponentName);
99
- document.body.appendChild(el);
100
- })();