@tp3/chat-widget 0.1.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.
@@ -0,0 +1,459 @@
1
+ // src/ChatWidget.tsx
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ function renderMarkdown(text) {
5
+ let html = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
7
+ html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
8
+ html = html.replace(/__(.+?)__/g, "<strong>$1</strong>");
9
+ html = html.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, "<em>$1</em>");
10
+ html = html.replace(
11
+ /\[([^\]]+)\]\(([^)]+)\)/g,
12
+ '<a href="$2" target="_blank" rel="noopener">$1</a>'
13
+ );
14
+ html = html.replace(/^#{1,4}\s+(.+)$/gm, "<strong>$1</strong>");
15
+ html = html.replace(/^[\t ]*[-*]\s+(.+)$/gm, "\u2022 $1");
16
+ html = html.replace(/\n\n/g, "<br><br>");
17
+ html = html.replace(/\n/g, "<br>");
18
+ return html;
19
+ }
20
+ var DEFAULTS = {
21
+ agentName: "tp3-chat-agent",
22
+ brandName: "Tp3studio",
23
+ brandSubtitle: "Asistente virtual",
24
+ welcomeMessage: "\u{1F44B} \xA1Hola! Soy el asistente de Tp3studio. \xBFEn qu\xE9 puedo ayudarte?"
25
+ };
26
+ function ChatWidget({
27
+ agentHost,
28
+ agentName = DEFAULTS.agentName,
29
+ brandName = DEFAULTS.brandName,
30
+ brandSubtitle = DEFAULTS.brandSubtitle,
31
+ welcomeMessage = DEFAULTS.welcomeMessage
32
+ }) {
33
+ const [open, setOpen] = useState(false);
34
+ const [closing, setClosing] = useState(false);
35
+ const [input, setInput] = useState("");
36
+ const [messages, setMessages] = useState(
37
+ []
38
+ );
39
+ const [loading, setLoading] = useState(false);
40
+ const wsRef = useRef(null);
41
+ const messagesEnd = useRef(null);
42
+ const inputRef = useRef(null);
43
+ const typewriterQueue = useRef([]);
44
+ const typewriterTimer = useRef(null);
45
+ useEffect(() => {
46
+ messagesEnd.current?.scrollIntoView({ behavior: "smooth" });
47
+ }, [messages]);
48
+ useEffect(() => {
49
+ if (!loading && open) {
50
+ inputRef.current?.focus();
51
+ }
52
+ }, [loading, open]);
53
+ const TYPEWRITER_MS = 180;
54
+ const streamDone = useRef(false);
55
+ function startTypewriter() {
56
+ let fullText = "";
57
+ typewriterTimer.current = setInterval(() => {
58
+ const queue = typewriterQueue.current;
59
+ if (queue.length > 0) {
60
+ fullText += queue.shift();
61
+ setMessages((prev) => {
62
+ const next = [...prev];
63
+ const last = next[next.length - 1];
64
+ if (last && last.role === "bot") {
65
+ next[next.length - 1] = { role: "bot", text: fullText };
66
+ } else {
67
+ next.push({ role: "bot", text: fullText });
68
+ }
69
+ return next;
70
+ });
71
+ return;
72
+ }
73
+ if (streamDone.current) {
74
+ clearInterval(typewriterTimer.current);
75
+ typewriterTimer.current = null;
76
+ setLoading(false);
77
+ }
78
+ }, TYPEWRITER_MS);
79
+ }
80
+ function connectWs() {
81
+ if (wsRef.current?.readyState === WebSocket.OPEN) return;
82
+ typewriterQueue.current = [];
83
+ streamDone.current = false;
84
+ if (typewriterTimer.current) {
85
+ clearInterval(typewriterTimer.current);
86
+ typewriterTimer.current = null;
87
+ }
88
+ const sid = crypto.randomUUID().slice(0, 8);
89
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
90
+ const ws = new WebSocket(
91
+ `${protocol}//${agentHost}/agents/${agentName}/${sid}`
92
+ );
93
+ wsRef.current = ws;
94
+ ws.onopen = () => {
95
+ if (messages.length === 0) {
96
+ setMessages([{ role: "bot", text: welcomeMessage }]);
97
+ }
98
+ };
99
+ ws.onmessage = (event) => {
100
+ try {
101
+ const data = JSON.parse(event.data);
102
+ if (data.type === "chat-response") {
103
+ streamDone.current = true;
104
+ setMessages((prev) => [
105
+ ...prev,
106
+ { role: "bot", text: data.message }
107
+ ]);
108
+ setLoading(false);
109
+ } else if (data.type === "chat-chunk") {
110
+ if (!data.done) {
111
+ if (!typewriterTimer.current) {
112
+ startTypewriter();
113
+ }
114
+ typewriterQueue.current.push(data.text || "");
115
+ } else {
116
+ streamDone.current = true;
117
+ }
118
+ }
119
+ } catch {
120
+ }
121
+ };
122
+ ws.onerror = () => {
123
+ streamDone.current = true;
124
+ if (typewriterTimer.current) {
125
+ clearInterval(typewriterTimer.current);
126
+ typewriterTimer.current = null;
127
+ }
128
+ setMessages((prev) => [
129
+ ...prev,
130
+ { role: "bot", text: "\u26A0 Error al conectar con el asistente." }
131
+ ]);
132
+ setLoading(false);
133
+ };
134
+ ws.onclose = () => {
135
+ streamDone.current = true;
136
+ if (typewriterTimer.current) {
137
+ clearInterval(typewriterTimer.current);
138
+ typewriterTimer.current = null;
139
+ }
140
+ wsRef.current = null;
141
+ };
142
+ }
143
+ function send() {
144
+ const text = input.trim();
145
+ if (!text || loading) return;
146
+ setInput("");
147
+ setMessages((prev) => [...prev, { role: "user", text }]);
148
+ setLoading(true);
149
+ const trySend = () => {
150
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
151
+ const history = messages.filter((m) => m.role !== "error").slice(-20).map((m) => ({
152
+ role: m.role === "bot" ? "assistant" : "user",
153
+ content: m.text
154
+ }));
155
+ wsRef.current.send(
156
+ JSON.stringify({ type: "chat", message: text, history })
157
+ );
158
+ } else {
159
+ setTimeout(trySend, 200);
160
+ }
161
+ };
162
+ trySend();
163
+ }
164
+ const chatWidth = 380;
165
+ const chatHeight = 540;
166
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
167
+ /* @__PURE__ */ jsx("style", { children: `
168
+ :root {
169
+ --chat-primary: #6366F1;
170
+ --chat-primary-fg: #fff;
171
+ --chat-primary-hover: #4F46E5;
172
+ --chat-bot-bubble: #F4F4F5;
173
+ --chat-bot-text: #18181B;
174
+ --chat-font-heading: "Outfit", sans-serif;
175
+ --chat-font-body: "Nunito", sans-serif;
176
+ --chat-border: #E4E4E7;
177
+ --chat-shadow: 0 8px 24px rgba(0,0,0,.12);
178
+ --chat-shadow-lg: 0 12px 48px rgba(0,0,0,.18);
179
+ --chat-code-bg: rgba(99,102,241,.12);
180
+ --chat-close-btn-bg: rgba(255,255,255,.1);
181
+ }
182
+ @keyframes slide-up { from { opacity:0; transform:translateY(16px) scale(0.96); } to { opacity:1; transform:translateY(0) scale(1); } }
183
+ @keyframes slide-down { from { opacity:1; transform:translateY(0) scale(1); } to { opacity:0; transform:translateY(16px) scale(0.96); } }
184
+ .chat-window { animation:slide-up .35s cubic-bezier(.16,1,.3,1) forwards; }
185
+ .chat-window-out { animation:slide-down .25s ease-in forwards; }
186
+ @keyframes typing-dot { 0%,60% { opacity:.2; transform:translateY(0); } 30% { opacity:1; transform:translateY(-6px); } 100% { opacity:.2; transform:translateY(0); } }
187
+ .typing-indicator { display:flex; align-items:center; gap:4px; padding:8px 14px; }
188
+ .typing-indicator span { width:7px; height:7px; border-radius:50%; background:var(--chat-primary); animation:typing-dot 1.4s infinite ease-in-out; }
189
+ .typing-indicator span:nth-child(2) { animation-delay:.15s; }
190
+ .typing-indicator span:nth-child(3) { animation-delay:.3s; }
191
+ .bot-msg a { color:var(--chat-primary); text-decoration:underline; }
192
+ .bot-msg a:hover { color:var(--chat-primary-hover); }
193
+ .bot-msg code { background:var(--chat-code-bg); color:var(--chat-primary); padding:1px 5px; border-radius:4px; font-size:.9em; font-family:monospace; }
194
+ .bot-msg strong { font-weight:600; color:var(--chat-bot-text); }
195
+ .bot-msg em { font-style:italic; }
196
+ @media (max-width: 480px) {
197
+ .chat-window, .chat-window-out {
198
+ width: 100% !important; height: 100% !important;
199
+ max-width: 100% !important; max-height: 100% !important;
200
+ bottom: 0 !important; right: 0 !important;
201
+ border-radius: 0 !important;
202
+ }
203
+ }
204
+ ` }),
205
+ !open && /* @__PURE__ */ jsx(
206
+ "button",
207
+ {
208
+ onClick: () => {
209
+ connectWs();
210
+ setOpen(true);
211
+ },
212
+ style: {
213
+ position: "fixed",
214
+ bottom: 24,
215
+ right: 24,
216
+ zIndex: 9999,
217
+ width: 56,
218
+ height: 56,
219
+ borderRadius: 16,
220
+ background: "var(--chat-primary)",
221
+ color: "var(--chat-primary-fg)",
222
+ border: "none",
223
+ cursor: "pointer",
224
+ display: "flex",
225
+ alignItems: "center",
226
+ justifyContent: "center",
227
+ boxShadow: "var(--chat-shadow)"
228
+ },
229
+ "aria-label": "Abrir chat",
230
+ children: /* @__PURE__ */ jsx(
231
+ "svg",
232
+ {
233
+ width: "22",
234
+ height: "22",
235
+ viewBox: "0 0 24 24",
236
+ fill: "none",
237
+ stroke: "currentColor",
238
+ strokeWidth: "2",
239
+ strokeLinecap: "round",
240
+ children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
241
+ }
242
+ )
243
+ }
244
+ ),
245
+ open && /* @__PURE__ */ jsxs(
246
+ "div",
247
+ {
248
+ className: closing ? "chat-window-out" : "chat-window",
249
+ style: {
250
+ position: "fixed",
251
+ bottom: 24,
252
+ right: 24,
253
+ zIndex: 9998,
254
+ width: chatWidth,
255
+ maxWidth: "calc(100vw - 48px)",
256
+ height: chatHeight,
257
+ maxHeight: "calc(100vh - 48px)",
258
+ background: "var(--chat-primary-fg)",
259
+ display: "flex",
260
+ flexDirection: "column",
261
+ borderRadius: 16,
262
+ overflow: "hidden",
263
+ boxShadow: "var(--chat-shadow-lg)"
264
+ },
265
+ children: [
266
+ /* @__PURE__ */ jsxs(
267
+ "div",
268
+ {
269
+ style: {
270
+ background: "var(--chat-primary)",
271
+ color: "var(--chat-primary-fg)",
272
+ padding: "14px 20px",
273
+ display: "flex",
274
+ alignItems: "center",
275
+ justifyContent: "space-between",
276
+ flexShrink: 0
277
+ },
278
+ children: [
279
+ /* @__PURE__ */ jsxs("div", { children: [
280
+ /* @__PURE__ */ jsx(
281
+ "div",
282
+ {
283
+ style: {
284
+ fontFamily: "var(--chat-font-heading)",
285
+ fontWeight: 600,
286
+ fontSize: 16
287
+ },
288
+ children: brandName
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, opacity: 0.75, marginTop: 1 }, children: brandSubtitle })
292
+ ] }),
293
+ /* @__PURE__ */ jsx(
294
+ "button",
295
+ {
296
+ onClick: () => {
297
+ wsRef.current?.close();
298
+ setClosing(true);
299
+ setTimeout(() => {
300
+ setOpen(false);
301
+ setClosing(false);
302
+ }, 250);
303
+ },
304
+ style: {
305
+ width: 32,
306
+ height: 32,
307
+ borderRadius: 12,
308
+ background: "var(--chat-close-btn-bg)",
309
+ border: "none",
310
+ color: "var(--chat-primary-fg)",
311
+ cursor: "pointer"
312
+ },
313
+ "aria-label": "Cerrar chat",
314
+ children: "\u2715"
315
+ }
316
+ )
317
+ ]
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsxs(
321
+ "div",
322
+ {
323
+ style: {
324
+ flex: 1,
325
+ overflowY: "auto",
326
+ padding: "12px 16px",
327
+ display: "flex",
328
+ flexDirection: "column",
329
+ gap: 10,
330
+ fontFamily: "var(--chat-font-body)"
331
+ },
332
+ children: [
333
+ messages.map((m, i) => /* @__PURE__ */ jsx(
334
+ "div",
335
+ {
336
+ className: m.role === "bot" ? "bot-msg" : "",
337
+ style: {
338
+ maxWidth: "85%",
339
+ padding: "10px 14px",
340
+ borderRadius: 16,
341
+ fontSize: 14,
342
+ lineHeight: 1.45,
343
+ alignSelf: m.role === "user" ? "flex-end" : "flex-start",
344
+ background: m.role === "user" ? "var(--chat-primary)" : "var(--chat-bot-bubble)",
345
+ color: m.role === "user" ? "var(--chat-primary-fg)" : "var(--chat-bot-text)",
346
+ borderBottomRightRadius: m.role === "user" ? 4 : 16,
347
+ borderBottomLeftRadius: m.role === "user" ? 16 : 4
348
+ },
349
+ children: m.role === "bot" ? /* @__PURE__ */ jsx(
350
+ "span",
351
+ {
352
+ dangerouslySetInnerHTML: {
353
+ __html: renderMarkdown(m.text)
354
+ }
355
+ }
356
+ ) : m.text
357
+ },
358
+ i
359
+ )),
360
+ loading && /* @__PURE__ */ jsxs(
361
+ "div",
362
+ {
363
+ className: "typing-indicator",
364
+ style: {
365
+ alignSelf: "flex-start",
366
+ background: "var(--chat-bot-bubble)",
367
+ borderRadius: 16,
368
+ borderBottomLeftRadius: 4
369
+ },
370
+ children: [
371
+ /* @__PURE__ */ jsx("span", {}),
372
+ /* @__PURE__ */ jsx("span", {}),
373
+ /* @__PURE__ */ jsx("span", {})
374
+ ]
375
+ }
376
+ ),
377
+ /* @__PURE__ */ jsx("div", { ref: messagesEnd })
378
+ ]
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsxs(
382
+ "form",
383
+ {
384
+ onSubmit: (e) => {
385
+ e.preventDefault();
386
+ send();
387
+ },
388
+ style: {
389
+ display: "flex",
390
+ gap: 8,
391
+ padding: "12px 16px 16px",
392
+ borderTop: "1px solid var(--chat-border)",
393
+ flexShrink: 0
394
+ },
395
+ children: [
396
+ /* @__PURE__ */ jsx(
397
+ "input",
398
+ {
399
+ ref: inputRef,
400
+ value: input,
401
+ onChange: (e) => setInput(e.target.value),
402
+ placeholder: "Escribe tu mensaje...",
403
+ disabled: loading,
404
+ style: {
405
+ flex: 1,
406
+ padding: "10px 14px",
407
+ border: "1px solid var(--chat-border)",
408
+ borderRadius: 12,
409
+ fontSize: 14,
410
+ outline: "none",
411
+ fontFamily: "var(--chat-font-body)"
412
+ }
413
+ }
414
+ ),
415
+ /* @__PURE__ */ jsx(
416
+ "button",
417
+ {
418
+ type: "submit",
419
+ disabled: loading || !input.trim(),
420
+ style: {
421
+ width: 40,
422
+ height: 40,
423
+ borderRadius: 12,
424
+ background: "var(--chat-primary)",
425
+ color: "var(--chat-primary-fg)",
426
+ border: "none",
427
+ cursor: "pointer",
428
+ opacity: loading || !input.trim() ? 0.4 : 1
429
+ },
430
+ title: "Enviar",
431
+ children: /* @__PURE__ */ jsxs(
432
+ "svg",
433
+ {
434
+ width: "18",
435
+ height: "18",
436
+ viewBox: "0 0 24 24",
437
+ fill: "none",
438
+ stroke: "currentColor",
439
+ strokeWidth: "2",
440
+ strokeLinecap: "round",
441
+ children: [
442
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
443
+ /* @__PURE__ */ jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
444
+ ]
445
+ }
446
+ )
447
+ }
448
+ )
449
+ ]
450
+ }
451
+ )
452
+ ]
453
+ }
454
+ )
455
+ ] });
456
+ }
457
+ export {
458
+ ChatWidget as default
459
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@tp3/chat-widget",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsup src/ChatWidget.tsx --format esm,cjs --dts",
15
+ "publish": "npm publish --access public"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.2.17",
19
+ "@types/react-dom": "^19.2.3",
20
+ "tsup": "^8.0.0",
21
+ "typescript": "^5.0.0"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.0.0"
25
+ }
26
+ }