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.
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,421 +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 prevOverflow = document.documentElement.style.overflow;
61
- const prevBodyOverflow = document.body.style.overflow;
62
-
63
- document.documentElement.style.overflow = "hidden";
64
- document.body.style.overflow = "hidden";
65
-
66
- return () => {
67
- document.documentElement.style.overflow = prevOverflow;
68
- document.body.style.overflow = prevBodyOverflow;
69
- };
70
- }, [open, isMobile]);
71
-
72
-
73
- return (
74
- <Box sx={anchorSx}>
75
- {!open && (
76
- <Button
77
- variant="contained"
78
- onClick={() => setOpen(true)}
79
- startIcon={<ChatBubbleOutlineRoundedIcon />}
80
- sx={{
81
- borderRadius: 999,
82
- px: 2,
83
- py: 1,
84
- textTransform: "none",
85
- boxShadow: "0 10px 30px rgba(0,0,0,.18)",
86
- }}
87
- >
88
- Chat
89
- </Button>
90
- )}
91
-
92
- {open && (
93
- <Paper
94
- elevation={isMobile ? 0 : 10}
95
- sx={{
96
- width: isMobile ? "100vw" : 380,
97
- height: isMobile ? "calc(var(--vh, 1vh) * 100)" : 560,
98
- "@supports (height: 100dvh)": {
99
- height: isMobile ? "100dvh" : 560,
100
- },
101
- maxWidth: "100vw",
102
- maxHeight: "100dvh",
103
- borderRadius: isMobile ? 0 : 4,
104
- position: isMobile ? "fixed" : "relative",
105
- inset: isMobile ? 0 : "auto",
106
- overflow: "hidden",
107
- border: isMobile ? "none" : "1px solid",
108
- borderColor: "divider",
109
- boxShadow: isMobile ? "none" : "0 18px 60px rgba(0,0,0,.28)",
110
- }}
111
- >
112
- <Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
113
-
114
- <Box
115
- sx={{
116
- px: 1.75,
117
- py: 1.25,
118
- display: "flex",
119
- alignItems: "center",
120
- gap: 1,
121
- borderBottom: "1px solid",
122
- borderColor: "divider",
123
- background:
124
- "linear-gradient(135deg, rgba(25,118,210,.18), rgba(156,39,176,.14))",
125
- backdropFilter: isMobile ? "none" : "blur(10px)",
126
- }}
127
- >
128
- <Badge
129
- overlap="circular"
130
- variant="dot"
131
- color="success"
132
- sx={{
133
- "& .MuiBadge-badge": {
134
- width: 10,
135
- height: 10,
136
- borderRadius: 999,
137
- border: "2px solid",
138
- borderColor: "background.paper",
139
- },
140
- }}
141
- >
142
- <Avatar sx={{ bgcolor: "primary.main" }}>
143
- <SmartToyRoundedIcon />
144
- </Avatar>
145
- </Badge>
146
-
147
- <Box sx={{ flex: 1, minWidth: 0 }}>
148
- <Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.1 }}>
149
- Asistent
150
- </Typography>
151
- <Typography variant="caption" sx={{ color: "text.secondary" }}>
152
- Online
153
- </Typography>
154
- </Box>
155
-
156
- <IconButton
157
- size="small"
158
- onClick={() => setOpen(false)}
159
- sx={{
160
- bgcolor: "rgba(255,255,255,.55)",
161
- border: "1px solid",
162
- borderColor: "divider",
163
- "&:hover": { bgcolor: "rgba(255,255,255,.75)" },
164
- }}
165
- >
166
- <CloseIcon fontSize="small" />
167
- </IconButton>
168
- </Box>
169
-
170
- <Box
171
- sx={{
172
- flex: 1,
173
- minHeight: 0,
174
- position: "relative",
175
- // height: 300,
176
- overflow: "auto",
177
- px: 1.5,
178
- py: 1.5,
179
- bgcolor: "background.default",
180
- backgroundImage:
181
- "radial-gradient(circle at 20% 10%, rgba(25,118,210,.10), transparent 45%)," +
182
- "radial-gradient(circle at 80% 20%, rgba(156,39,176,.10), transparent 50%)," +
183
- "radial-gradient(circle at 40% 90%, rgba(0,200,83,.08), transparent 45%)",
184
- "&::-webkit-scrollbar": { width: 10 },
185
- "&::-webkit-scrollbar-thumb": {
186
- backgroundColor: "rgba(0,0,0,.18)",
187
- borderRadius: 999,
188
- border: "3px solid transparent",
189
- backgroundClip: "content-box",
190
- },
191
- overscrollBehavior: "contain",
192
- WebkitOverflowScrolling: "touch",
193
- }}
194
- >
195
- {msgs.map((m) => {
196
- const isUser = m.role === "user";
197
- return (
198
- <Box
199
- key={m.id}
200
- sx={{
201
- display: "flex",
202
- justifyContent: isUser ? "flex-end" : "flex-start",
203
- gap: 1,
204
- mb: 1.2,
205
- }}
206
- >
207
- {!isUser && (
208
- <Avatar
209
- sx={{
210
- width: 30,
211
- height: 30,
212
- mt: 0.25,
213
- bgcolor: "primary.main",
214
- }}
215
- >
216
- <SmartToyRoundedIcon sx={{ fontSize: 18 }} />
217
- </Avatar>
218
- )}
219
-
220
- <Box sx={{ maxWidth: "78%" }}>
221
- {m.timestamp && (
222
- <Box sx={{ display: "flex", justifyContent: isUser ? "flex-end" : "flex-start", mb: 0.5 }}>
223
- <Chip size="small" label={m.timestamp} variant="outlined" sx={{ height: 22 }} />
224
- </Box>
225
- )}
226
-
227
- <Box
228
- sx={{
229
- position: "relative",
230
- px: 1.4,
231
- py: 1.05,
232
- borderRadius: 3,
233
- border: "1px solid",
234
- borderColor: "divider",
235
- bgcolor: isUser ? "primary.main" : "background.paper",
236
- color: isUser ? "primary.contrastText" : "text.primary",
237
- boxShadow: isUser
238
- ? "0 10px 30px rgba(25,118,210,.22)"
239
- : "0 10px 30px rgba(0,0,0,.10)",
240
- whiteSpace: "pre-wrap",
241
- "&:before": {
242
- content: '""',
243
- position: "absolute",
244
- top: 14,
245
- width: 10,
246
- height: 10,
247
- transform: "rotate(45deg)",
248
- bgcolor: isUser ? "primary.main" : "background.paper",
249
- borderLeft: isUser ? "none" : "1px solid",
250
- borderTop: isUser ? "none" : "1px solid",
251
- borderColor: "divider",
252
- right: isUser ? -5 : "auto",
253
- left: isUser ? "auto" : -5,
254
- },
255
- "& p": { margin: 0 },
256
- "& a": { color: "inherit", textDecoration: "underline" },
257
- "& code": {
258
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
259
- fontSize: "0.9em",
260
- padding: "2px 6px",
261
- borderRadius: 8,
262
- backgroundColor: isUser ? "rgba(255,255,255,.14)" : "rgba(0,0,0,.06)",
263
- },
264
- "& pre": {
265
- margin: 0,
266
- marginTop: 8,
267
- padding: 12,
268
- overflow: "auto",
269
- borderRadius: 12,
270
- backgroundColor: isUser ? "rgba(255,255,255,.12)" : "rgba(0,0,0,.05)",
271
- },
272
- }}
273
- >
274
- <ReactMarkdown>{m.text}</ReactMarkdown>
275
- </Box>
276
- </Box>
277
-
278
- {isUser && (
279
- <Avatar
280
- sx={{
281
- width: 30,
282
- height: 30,
283
- mt: 0.25,
284
- bgcolor: "grey.900",
285
- }}
286
- >
287
- <PersonRoundedIcon sx={{ fontSize: 18 }} />
288
- </Avatar>
289
- )}
290
- </Box>
291
- );
292
- })}
293
-
294
- {sending && (
295
- <Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, mb: 1.2 }}>
296
- <Avatar sx={{ width: 30, height: 30, bgcolor: "primary.main" }}>
297
- <SmartToyRoundedIcon sx={{ fontSize: 18 }} />
298
- </Avatar>
299
-
300
- <Box
301
- sx={{
302
- px: 1.4,
303
- py: 1.1,
304
- borderRadius: 3,
305
- border: "1px solid",
306
- borderColor: "divider",
307
- bgcolor: "background.paper",
308
- boxShadow: "0 10px 30px rgba(0,0,0,.10)",
309
- display: "flex",
310
- alignItems: "center",
311
- gap: 0.6,
312
- }}
313
- >
314
- <Box
315
- sx={{
316
- width: 6,
317
- height: 6,
318
- borderRadius: 999,
319
- bgcolor: "text.secondary",
320
- animation: "dot 1.1s infinite ease-in-out",
321
- "@keyframes dot": {
322
- "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
323
- "40%": { transform: "scale(1)", opacity: 1 },
324
- },
325
- }}
326
- />
327
- <Box
328
- sx={{
329
- width: 6,
330
- height: 6,
331
- borderRadius: 999,
332
- bgcolor: "text.secondary",
333
- animation: "dot 1.1s infinite ease-in-out",
334
- animationDelay: "0.15s",
335
- "@keyframes dot": {
336
- "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
337
- "40%": { transform: "scale(1)", opacity: 1 },
338
- },
339
- }}
340
- />
341
- <Box
342
- sx={{
343
- width: 6,
344
- height: 6,
345
- borderRadius: 999,
346
- bgcolor: "text.secondary",
347
- animation: "dot 1.1s infinite ease-in-out",
348
- animationDelay: "0.3s",
349
- "@keyframes dot": {
350
- "0%, 80%, 100%": { transform: "scale(0.6)", opacity: 0.4 },
351
- "40%": { transform: "scale(1)", opacity: 1 },
352
- },
353
- }}
354
- />
355
- </Box>
356
- </Box>
357
- )}
358
-
359
- <div ref={endRef} />
360
- </Box>
361
-
362
- <Box
363
- sx={{
364
- position: "sticky",
365
- bottom: 0,
366
- zIndex: 2,
367
- p: 1.25,
368
- borderTop: "1px solid",
369
- borderColor: "divider",
370
- display: "flex",
371
- gap: 1,
372
- bgcolor: "background.paper",
373
- }}
374
- >
375
- <TextField
376
- size="small"
377
- fullWidth
378
- value={input}
379
- disabled={sending}
380
- onChange={(e) => setInput(e.target.value)}
381
- onKeyDown={(e) => {
382
- if (e.key === "Enter" && !e.shiftKey) {
383
- e.preventDefault();
384
- send();
385
- }
386
- }}
387
- placeholder="Scrie un mesaj"
388
- multiline
389
- maxRows={4}
390
- sx={{
391
- "& .MuiOutlinedInput-root": {
392
- borderRadius: 999,
393
- pr: 0.75,
394
- backgroundColor: "rgba(0,0,0,.02)",
395
- },
396
- }}
397
- InputProps={{
398
- endAdornment: (
399
- <IconButton
400
- size="small"
401
- onClick={send}
402
- disabled={sending || !input.trim()}
403
- sx={{
404
- ml: 0.5,
405
- borderRadius: 999,
406
- }}
407
- >
408
- <SendRoundedIcon />
409
- </IconButton>
410
- ),
411
- }}
412
- />
413
- </Box>
414
-
415
- </Box>
416
- </Paper>
417
- )}
418
- </Box>
419
- );
420
- }
421
- 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
- })();