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