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