@kayro-ia/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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +717 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kairo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @kayro-ia/widget
|
|
2
|
+
|
|
3
|
+
Embeddable AI chat widget for your product — powered by [Kairo](https://kayro.com.ar).
|
|
4
|
+
|
|
5
|
+
Add a fully functional AI assistant to your app in 3 lines of code.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @kayro-ia/widget
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { AIWidget } from '@kayro-ia/widget'
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
return (
|
|
20
|
+
<AIWidget
|
|
21
|
+
apiKey="your_api_key"
|
|
22
|
+
orchestratorUrl="https://kayro.com.ar"
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
| Prop | Type | Required | Default | Description |
|
|
31
|
+
|------|------|----------|---------|-------------|
|
|
32
|
+
| `apiKey` | `string` | ✅ | — | Your Kairo API key |
|
|
33
|
+
| `orchestratorUrl` | `string` | ✅ | — | Kairo orchestrator base URL |
|
|
34
|
+
| `title` | `string` | | `"Asistente"` | Chat window header title |
|
|
35
|
+
| `placeholder` | `string` | | `"Escribí tu mensaje..."` | Input placeholder |
|
|
36
|
+
| `primaryColor` | `string` | | `"#6366f1"` | Accent color (bubble + user messages) |
|
|
37
|
+
| `position` | `"bottom-right" \| "bottom-left"` | | `"bottom-right"` | Widget position |
|
|
38
|
+
| `initialOpen` | `boolean` | | `false` | Open the chat window on load |
|
|
39
|
+
| `zIndex` | `number` | | `9999` | CSS z-index |
|
|
40
|
+
| `userToken` | `string` | | — | End-user auth token (forwarded to your MCP server) |
|
|
41
|
+
| `authHeaders` | `Record<string, string>` | | — | Extra headers forwarded to your MCP server |
|
|
42
|
+
| `userId` | `string` | | — | End-user ID (for analytics) |
|
|
43
|
+
| `userName` | `string` | | — | End-user display name |
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- React 18+
|
|
48
|
+
- Modern browser (Chrome 92+, Firefox 90+, Safari 15+)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface Message {
|
|
4
|
+
id: string;
|
|
5
|
+
role: 'user' | 'assistant';
|
|
6
|
+
content: string;
|
|
7
|
+
status: 'sending' | 'streaming' | 'done' | 'error';
|
|
8
|
+
tokensUsed?: number;
|
|
9
|
+
}
|
|
10
|
+
interface WidgetConfig {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
orchestratorUrl?: string;
|
|
13
|
+
userToken?: string;
|
|
14
|
+
authHeaders?: Record<string, string>;
|
|
15
|
+
userId?: string;
|
|
16
|
+
userName?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
primaryColor?: string;
|
|
20
|
+
position?: 'bottom-right' | 'bottom-left';
|
|
21
|
+
initialOpen?: boolean;
|
|
22
|
+
zIndex?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare function AIWidget(props: WidgetConfig): react_jsx_runtime.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { AIWidget, type Message, type WidgetConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
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/index.tsx
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AIWidget: () => AIWidget
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/AIWidget.tsx
|
|
28
|
+
var import_react4 = require("react");
|
|
29
|
+
|
|
30
|
+
// src/store/chatStore.ts
|
|
31
|
+
var import_zustand = require("zustand");
|
|
32
|
+
var useChatStore = (0, import_zustand.create)((set) => ({
|
|
33
|
+
messages: [],
|
|
34
|
+
isOpen: false,
|
|
35
|
+
isStreaming: false,
|
|
36
|
+
conversationId: crypto.randomUUID(),
|
|
37
|
+
addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),
|
|
38
|
+
updateLastAssistantMessage: (delta) => set((state) => {
|
|
39
|
+
const messages = [...state.messages];
|
|
40
|
+
const lastIdx = messages.length - 1;
|
|
41
|
+
if (lastIdx < 0 || messages[lastIdx].role !== "assistant") return state;
|
|
42
|
+
messages[lastIdx] = {
|
|
43
|
+
...messages[lastIdx],
|
|
44
|
+
content: messages[lastIdx].content + delta
|
|
45
|
+
};
|
|
46
|
+
return { messages };
|
|
47
|
+
}),
|
|
48
|
+
finalizeLastAssistantMessage: (tokensUsed) => set((state) => {
|
|
49
|
+
const messages = [...state.messages];
|
|
50
|
+
const lastIdx = messages.length - 1;
|
|
51
|
+
if (lastIdx < 0 || messages[lastIdx].role !== "assistant") return state;
|
|
52
|
+
messages[lastIdx] = {
|
|
53
|
+
...messages[lastIdx],
|
|
54
|
+
status: "done",
|
|
55
|
+
tokensUsed
|
|
56
|
+
};
|
|
57
|
+
return { messages };
|
|
58
|
+
}),
|
|
59
|
+
setLastAssistantError: () => set((state) => {
|
|
60
|
+
const messages = [...state.messages];
|
|
61
|
+
const lastIdx = messages.length - 1;
|
|
62
|
+
if (lastIdx < 0 || messages[lastIdx].role !== "assistant") return state;
|
|
63
|
+
messages[lastIdx] = {
|
|
64
|
+
...messages[lastIdx],
|
|
65
|
+
status: "error"
|
|
66
|
+
};
|
|
67
|
+
return { messages };
|
|
68
|
+
}),
|
|
69
|
+
setStreaming: (v) => set({ isStreaming: v }),
|
|
70
|
+
setOpen: (v) => set({ isOpen: v }),
|
|
71
|
+
clearMessages: () => set({ messages: [] })
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// src/components/ChatBubble.tsx
|
|
75
|
+
var import_jsx_runtime = (
|
|
76
|
+
// Close (×) icon
|
|
77
|
+
require("react/jsx-runtime")
|
|
78
|
+
);
|
|
79
|
+
function ChatBubble({ isOpen, onClick, primaryColor, position, zIndex }) {
|
|
80
|
+
const positionStyle = position === "bottom-right" ? { right: 24, bottom: 24 } : { left: 24, bottom: 24 };
|
|
81
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
82
|
+
"button",
|
|
83
|
+
{
|
|
84
|
+
onClick,
|
|
85
|
+
"aria-label": isOpen ? "Cerrar chat" : "Abrir chat",
|
|
86
|
+
style: {
|
|
87
|
+
position: "fixed",
|
|
88
|
+
...positionStyle,
|
|
89
|
+
width: 56,
|
|
90
|
+
height: 56,
|
|
91
|
+
borderRadius: "50%",
|
|
92
|
+
backgroundColor: primaryColor,
|
|
93
|
+
border: "none",
|
|
94
|
+
cursor: "pointer",
|
|
95
|
+
display: "flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
justifyContent: "center",
|
|
98
|
+
boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
|
|
99
|
+
zIndex,
|
|
100
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
101
|
+
color: "#ffffff",
|
|
102
|
+
padding: 0
|
|
103
|
+
},
|
|
104
|
+
onMouseEnter: (e) => {
|
|
105
|
+
e.currentTarget.style.transform = "scale(1.08)";
|
|
106
|
+
e.currentTarget.style.boxShadow = "0 6px 24px rgba(0,0,0,0.28)";
|
|
107
|
+
},
|
|
108
|
+
onMouseLeave: (e) => {
|
|
109
|
+
e.currentTarget.style.transform = "scale(1)";
|
|
110
|
+
e.currentTarget.style.boxShadow = "0 4px 20px rgba(0,0,0,0.2)";
|
|
111
|
+
},
|
|
112
|
+
children: isOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
113
|
+
"svg",
|
|
114
|
+
{
|
|
115
|
+
width: "22",
|
|
116
|
+
height: "22",
|
|
117
|
+
viewBox: "0 0 24 24",
|
|
118
|
+
fill: "none",
|
|
119
|
+
stroke: "currentColor",
|
|
120
|
+
strokeWidth: "2.5",
|
|
121
|
+
strokeLinecap: "round",
|
|
122
|
+
strokeLinejoin: "round",
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
) : (
|
|
129
|
+
// Chat bubble icon
|
|
130
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
131
|
+
"svg",
|
|
132
|
+
{
|
|
133
|
+
width: "24",
|
|
134
|
+
height: "24",
|
|
135
|
+
viewBox: "0 0 24 24",
|
|
136
|
+
fill: "currentColor",
|
|
137
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z" })
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/hooks/useChat.ts
|
|
146
|
+
function normalizeUrl(base) {
|
|
147
|
+
return base.endsWith("/") ? base.slice(0, -1) : base;
|
|
148
|
+
}
|
|
149
|
+
function useChat() {
|
|
150
|
+
const {
|
|
151
|
+
messages,
|
|
152
|
+
isStreaming,
|
|
153
|
+
conversationId,
|
|
154
|
+
addMessage,
|
|
155
|
+
updateLastAssistantMessage,
|
|
156
|
+
finalizeLastAssistantMessage,
|
|
157
|
+
setLastAssistantError,
|
|
158
|
+
setStreaming
|
|
159
|
+
} = useChatStore();
|
|
160
|
+
async function sendMessage(content, config) {
|
|
161
|
+
var _a, _b, _c;
|
|
162
|
+
if (isStreaming || !content.trim()) return;
|
|
163
|
+
const userMessage = {
|
|
164
|
+
id: crypto.randomUUID(),
|
|
165
|
+
role: "user",
|
|
166
|
+
content: content.trim(),
|
|
167
|
+
status: "done"
|
|
168
|
+
};
|
|
169
|
+
addMessage(userMessage);
|
|
170
|
+
const assistantMessage = {
|
|
171
|
+
id: crypto.randomUUID(),
|
|
172
|
+
role: "assistant",
|
|
173
|
+
content: "",
|
|
174
|
+
status: "streaming"
|
|
175
|
+
};
|
|
176
|
+
addMessage(assistantMessage);
|
|
177
|
+
setStreaming(true);
|
|
178
|
+
const baseUrl = normalizeUrl((_a = config.orchestratorUrl) != null ? _a : "https://app.kayro.com.ar");
|
|
179
|
+
const history = [...messages, userMessage].map((m) => ({
|
|
180
|
+
role: m.role,
|
|
181
|
+
content: m.content
|
|
182
|
+
}));
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
"X-API-Key": config.apiKey
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
apiKey: config.apiKey,
|
|
192
|
+
conversationId,
|
|
193
|
+
messages: history,
|
|
194
|
+
...config.userToken !== void 0 && { userToken: config.userToken },
|
|
195
|
+
...config.authHeaders !== void 0 && { authHeaders: config.authHeaders },
|
|
196
|
+
...config.userId !== void 0 && { userId: config.userId },
|
|
197
|
+
...config.userName !== void 0 && { userName: config.userName }
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
202
|
+
}
|
|
203
|
+
if (!response.body) {
|
|
204
|
+
throw new Error("Response body is null");
|
|
205
|
+
}
|
|
206
|
+
const reader = response.body.getReader();
|
|
207
|
+
const decoder = new TextDecoder();
|
|
208
|
+
let buffer = "";
|
|
209
|
+
while (true) {
|
|
210
|
+
const { done, value } = await reader.read();
|
|
211
|
+
if (done) break;
|
|
212
|
+
buffer += decoder.decode(value, { stream: true });
|
|
213
|
+
const chunks = buffer.split("\n\n");
|
|
214
|
+
buffer = (_b = chunks.pop()) != null ? _b : "";
|
|
215
|
+
for (const chunk of chunks) {
|
|
216
|
+
for (const line of chunk.split("\n")) {
|
|
217
|
+
if (!line.startsWith("data: ")) continue;
|
|
218
|
+
const raw = line.slice(6).trim();
|
|
219
|
+
if (!raw) continue;
|
|
220
|
+
let data;
|
|
221
|
+
try {
|
|
222
|
+
data = JSON.parse(raw);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (data.type === "text_delta" && data.delta !== void 0) {
|
|
227
|
+
updateLastAssistantMessage(data.delta);
|
|
228
|
+
} else if (data.type === "text" && data.text !== void 0) {
|
|
229
|
+
updateLastAssistantMessage(data.text);
|
|
230
|
+
} else if (data.type === "done") {
|
|
231
|
+
const tokensUsed = (_c = data.tokensUsed) != null ? _c : data.tokens_used;
|
|
232
|
+
finalizeLastAssistantMessage(tokensUsed);
|
|
233
|
+
} else if (data.type === "error") {
|
|
234
|
+
setLastAssistantError();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
finalizeLastAssistantMessage();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error("[AIWidget] sendMessage error:", err);
|
|
242
|
+
setLastAssistantError();
|
|
243
|
+
} finally {
|
|
244
|
+
setStreaming(false);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { sendMessage };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/components/MessageList.tsx
|
|
251
|
+
var import_react2 = require("react");
|
|
252
|
+
|
|
253
|
+
// src/components/TypingIndicator.tsx
|
|
254
|
+
var import_react = require("react");
|
|
255
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
256
|
+
var DOT_STYLE = {
|
|
257
|
+
width: 8,
|
|
258
|
+
height: 8,
|
|
259
|
+
borderRadius: "50%",
|
|
260
|
+
backgroundColor: "#9ca3af",
|
|
261
|
+
display: "inline-block",
|
|
262
|
+
margin: "0 2px"
|
|
263
|
+
};
|
|
264
|
+
function TypingIndicator() {
|
|
265
|
+
const [frame, setFrame] = (0, import_react.useState)(0);
|
|
266
|
+
(0, import_react.useEffect)(() => {
|
|
267
|
+
const interval = setInterval(() => {
|
|
268
|
+
setFrame((f) => (f + 1) % 3);
|
|
269
|
+
}, 400);
|
|
270
|
+
return () => clearInterval(interval);
|
|
271
|
+
}, []);
|
|
272
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
273
|
+
"div",
|
|
274
|
+
{
|
|
275
|
+
style: {
|
|
276
|
+
display: "flex",
|
|
277
|
+
alignItems: "center",
|
|
278
|
+
padding: "10px 14px",
|
|
279
|
+
backgroundColor: "#f3f4f6",
|
|
280
|
+
borderRadius: "16px 16px 16px 4px",
|
|
281
|
+
width: "fit-content",
|
|
282
|
+
maxWidth: 80
|
|
283
|
+
},
|
|
284
|
+
children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
285
|
+
"span",
|
|
286
|
+
{
|
|
287
|
+
style: {
|
|
288
|
+
...DOT_STYLE,
|
|
289
|
+
opacity: frame === i ? 1 : 0.35,
|
|
290
|
+
transform: frame === i ? "translateY(-2px)" : "translateY(0)",
|
|
291
|
+
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
i
|
|
295
|
+
))
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/components/MessageItem.tsx
|
|
301
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
302
|
+
function renderContent(text) {
|
|
303
|
+
const nodes = [];
|
|
304
|
+
const parts = text.split(/(\*\*[^*]+\*\*|\n)/g);
|
|
305
|
+
parts.forEach((part, i) => {
|
|
306
|
+
if (part.startsWith("**") && part.endsWith("**") && part.length > 4) {
|
|
307
|
+
nodes.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: part.slice(2, -2) }, i));
|
|
308
|
+
} else if (part === "\n") {
|
|
309
|
+
nodes.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("br", {}, i));
|
|
310
|
+
} else {
|
|
311
|
+
nodes.push(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: part }, i));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return nodes;
|
|
315
|
+
}
|
|
316
|
+
function MessageItem({ message, primaryColor }) {
|
|
317
|
+
const isUser = message.role === "user";
|
|
318
|
+
const bubbleStyle = isUser ? {
|
|
319
|
+
backgroundColor: primaryColor,
|
|
320
|
+
color: "#ffffff",
|
|
321
|
+
borderRadius: "16px 16px 4px 16px",
|
|
322
|
+
padding: "10px 14px",
|
|
323
|
+
maxWidth: "75%",
|
|
324
|
+
wordBreak: "break-word",
|
|
325
|
+
fontSize: 14,
|
|
326
|
+
lineHeight: "1.5"
|
|
327
|
+
} : {
|
|
328
|
+
backgroundColor: "#f3f4f6",
|
|
329
|
+
color: "#111827",
|
|
330
|
+
borderRadius: "16px 16px 16px 4px",
|
|
331
|
+
padding: "10px 14px",
|
|
332
|
+
maxWidth: "75%",
|
|
333
|
+
wordBreak: "break-word",
|
|
334
|
+
fontSize: 14,
|
|
335
|
+
lineHeight: "1.5"
|
|
336
|
+
};
|
|
337
|
+
if (message.status === "streaming" && message.content === "") {
|
|
338
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
339
|
+
"div",
|
|
340
|
+
{
|
|
341
|
+
style: {
|
|
342
|
+
display: "flex",
|
|
343
|
+
justifyContent: "flex-start",
|
|
344
|
+
marginBottom: 8
|
|
345
|
+
},
|
|
346
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TypingIndicator, {})
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (message.status === "error") {
|
|
351
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
style: {
|
|
355
|
+
display: "flex",
|
|
356
|
+
justifyContent: isUser ? "flex-end" : "flex-start",
|
|
357
|
+
marginBottom: 8
|
|
358
|
+
},
|
|
359
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
360
|
+
"div",
|
|
361
|
+
{
|
|
362
|
+
style: {
|
|
363
|
+
...bubbleStyle,
|
|
364
|
+
backgroundColor: "#fee2e2",
|
|
365
|
+
color: "#dc2626",
|
|
366
|
+
borderRadius: "16px"
|
|
367
|
+
},
|
|
368
|
+
children: "Error al procesar el mensaje. Por favor, intent\xE1 de nuevo."
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
375
|
+
"div",
|
|
376
|
+
{
|
|
377
|
+
style: {
|
|
378
|
+
display: "flex",
|
|
379
|
+
justifyContent: isUser ? "flex-end" : "flex-start",
|
|
380
|
+
marginBottom: 8
|
|
381
|
+
},
|
|
382
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: bubbleStyle, children: renderContent(message.content) })
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/components/MessageList.tsx
|
|
388
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
389
|
+
function MessageList({ messages, primaryColor }) {
|
|
390
|
+
const bottomRef = (0, import_react2.useRef)(null);
|
|
391
|
+
(0, import_react2.useEffect)(() => {
|
|
392
|
+
var _a;
|
|
393
|
+
(_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
394
|
+
}, [messages]);
|
|
395
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
396
|
+
"div",
|
|
397
|
+
{
|
|
398
|
+
style: {
|
|
399
|
+
flex: 1,
|
|
400
|
+
overflowY: "auto",
|
|
401
|
+
padding: "16px 16px 8px",
|
|
402
|
+
display: "flex",
|
|
403
|
+
flexDirection: "column"
|
|
404
|
+
},
|
|
405
|
+
children: [
|
|
406
|
+
messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
407
|
+
"div",
|
|
408
|
+
{
|
|
409
|
+
style: {
|
|
410
|
+
flex: 1,
|
|
411
|
+
display: "flex",
|
|
412
|
+
alignItems: "center",
|
|
413
|
+
justifyContent: "center",
|
|
414
|
+
color: "#9ca3af",
|
|
415
|
+
fontSize: 14,
|
|
416
|
+
textAlign: "center",
|
|
417
|
+
padding: "0 24px"
|
|
418
|
+
},
|
|
419
|
+
children: "\xBFEn qu\xE9 te puedo ayudar?"
|
|
420
|
+
}
|
|
421
|
+
),
|
|
422
|
+
messages.map((msg) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MessageItem, { message: msg, primaryColor }, msg.id)),
|
|
423
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: bottomRef })
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/components/InputBar.tsx
|
|
430
|
+
var import_react3 = require("react");
|
|
431
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
432
|
+
function InputBar({ onSend, isStreaming, placeholder, primaryColor }) {
|
|
433
|
+
const [value, setValue] = (0, import_react3.useState)("");
|
|
434
|
+
const textareaRef = (0, import_react3.useRef)(null);
|
|
435
|
+
function handleSend() {
|
|
436
|
+
const trimmed = value.trim();
|
|
437
|
+
if (!trimmed || isStreaming) return;
|
|
438
|
+
onSend(trimmed);
|
|
439
|
+
setValue("");
|
|
440
|
+
if (textareaRef.current) {
|
|
441
|
+
textareaRef.current.style.height = "auto";
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function handleKeyDown(e) {
|
|
445
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
handleSend();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function handleInput() {
|
|
451
|
+
const el = textareaRef.current;
|
|
452
|
+
if (!el) return;
|
|
453
|
+
el.style.height = "auto";
|
|
454
|
+
el.style.height = `${Math.min(el.scrollHeight, 72)}px`;
|
|
455
|
+
}
|
|
456
|
+
const disabled = isStreaming || !value.trim();
|
|
457
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
458
|
+
"div",
|
|
459
|
+
{
|
|
460
|
+
style: {
|
|
461
|
+
display: "flex",
|
|
462
|
+
alignItems: "flex-end",
|
|
463
|
+
gap: 8,
|
|
464
|
+
padding: "12px 16px",
|
|
465
|
+
borderTop: "1px solid #e5e7eb",
|
|
466
|
+
backgroundColor: "#ffffff"
|
|
467
|
+
},
|
|
468
|
+
children: [
|
|
469
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
470
|
+
"textarea",
|
|
471
|
+
{
|
|
472
|
+
ref: textareaRef,
|
|
473
|
+
value,
|
|
474
|
+
onChange: (e) => setValue(e.target.value),
|
|
475
|
+
onKeyDown: handleKeyDown,
|
|
476
|
+
onInput: handleInput,
|
|
477
|
+
placeholder,
|
|
478
|
+
rows: 1,
|
|
479
|
+
disabled: isStreaming,
|
|
480
|
+
style: {
|
|
481
|
+
flex: 1,
|
|
482
|
+
resize: "none",
|
|
483
|
+
border: "1px solid #e5e7eb",
|
|
484
|
+
borderRadius: 12,
|
|
485
|
+
padding: "8px 12px",
|
|
486
|
+
fontSize: 14,
|
|
487
|
+
lineHeight: "24px",
|
|
488
|
+
outline: "none",
|
|
489
|
+
fontFamily: "inherit",
|
|
490
|
+
color: "#111827",
|
|
491
|
+
backgroundColor: isStreaming ? "#f9fafb" : "#ffffff",
|
|
492
|
+
transition: "border-color 0.15s ease",
|
|
493
|
+
overflowY: "hidden"
|
|
494
|
+
},
|
|
495
|
+
onFocus: (e) => {
|
|
496
|
+
e.currentTarget.style.borderColor = primaryColor;
|
|
497
|
+
},
|
|
498
|
+
onBlur: (e) => {
|
|
499
|
+
e.currentTarget.style.borderColor = "#e5e7eb";
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
504
|
+
"button",
|
|
505
|
+
{
|
|
506
|
+
onClick: handleSend,
|
|
507
|
+
disabled,
|
|
508
|
+
"aria-label": "Enviar mensaje",
|
|
509
|
+
style: {
|
|
510
|
+
width: 36,
|
|
511
|
+
height: 36,
|
|
512
|
+
borderRadius: "50%",
|
|
513
|
+
border: "none",
|
|
514
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
515
|
+
backgroundColor: disabled ? "#e5e7eb" : primaryColor,
|
|
516
|
+
color: "#ffffff",
|
|
517
|
+
display: "flex",
|
|
518
|
+
alignItems: "center",
|
|
519
|
+
justifyContent: "center",
|
|
520
|
+
flexShrink: 0,
|
|
521
|
+
transition: "background-color 0.15s ease",
|
|
522
|
+
padding: 0
|
|
523
|
+
},
|
|
524
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
525
|
+
"svg",
|
|
526
|
+
{
|
|
527
|
+
width: "16",
|
|
528
|
+
height: "16",
|
|
529
|
+
viewBox: "0 0 24 24",
|
|
530
|
+
fill: "none",
|
|
531
|
+
stroke: "currentColor",
|
|
532
|
+
strokeWidth: "2",
|
|
533
|
+
strokeLinecap: "round",
|
|
534
|
+
strokeLinejoin: "round",
|
|
535
|
+
children: [
|
|
536
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
|
|
537
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/components/ChatWindow.tsx
|
|
549
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
550
|
+
function ChatWindow({ config }) {
|
|
551
|
+
var _a, _b, _c, _d, _e;
|
|
552
|
+
const { messages, isStreaming, setOpen } = useChatStore();
|
|
553
|
+
const { sendMessage } = useChat();
|
|
554
|
+
const title = (_a = config.title) != null ? _a : "Asistente";
|
|
555
|
+
const placeholder = (_b = config.placeholder) != null ? _b : "Escrib\xED tu mensaje...";
|
|
556
|
+
const primaryColor = (_c = config.primaryColor) != null ? _c : "#6366f1";
|
|
557
|
+
const position = (_d = config.position) != null ? _d : "bottom-right";
|
|
558
|
+
const positionStyle = position === "bottom-right" ? { right: 24, bottom: 88 } : { left: 24, bottom: 88 };
|
|
559
|
+
function handleSend(content) {
|
|
560
|
+
sendMessage(content, config);
|
|
561
|
+
}
|
|
562
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
563
|
+
"div",
|
|
564
|
+
{
|
|
565
|
+
style: {
|
|
566
|
+
position: "fixed",
|
|
567
|
+
...positionStyle,
|
|
568
|
+
width: 380,
|
|
569
|
+
height: 520,
|
|
570
|
+
backgroundColor: "#ffffff",
|
|
571
|
+
borderRadius: 16,
|
|
572
|
+
boxShadow: "0 20px 60px rgba(0,0,0,0.15), 0 4px 16px rgba(0,0,0,0.08)",
|
|
573
|
+
display: "flex",
|
|
574
|
+
flexDirection: "column",
|
|
575
|
+
overflow: "hidden",
|
|
576
|
+
zIndex: (_e = config.zIndex) != null ? _e : 9999,
|
|
577
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
|
578
|
+
},
|
|
579
|
+
children: [
|
|
580
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
581
|
+
"div",
|
|
582
|
+
{
|
|
583
|
+
style: {
|
|
584
|
+
backgroundColor: primaryColor,
|
|
585
|
+
color: "#ffffff",
|
|
586
|
+
padding: "16px 20px",
|
|
587
|
+
display: "flex",
|
|
588
|
+
alignItems: "center",
|
|
589
|
+
justifyContent: "space-between",
|
|
590
|
+
flexShrink: 0
|
|
591
|
+
},
|
|
592
|
+
children: [
|
|
593
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
|
|
594
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
595
|
+
"div",
|
|
596
|
+
{
|
|
597
|
+
style: {
|
|
598
|
+
width: 32,
|
|
599
|
+
height: 32,
|
|
600
|
+
borderRadius: "50%",
|
|
601
|
+
backgroundColor: "rgba(255,255,255,0.25)",
|
|
602
|
+
display: "flex",
|
|
603
|
+
alignItems: "center",
|
|
604
|
+
justifyContent: "center"
|
|
605
|
+
},
|
|
606
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
607
|
+
"svg",
|
|
608
|
+
{
|
|
609
|
+
width: "16",
|
|
610
|
+
height: "16",
|
|
611
|
+
viewBox: "0 0 24 24",
|
|
612
|
+
fill: "currentColor",
|
|
613
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z" })
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
),
|
|
618
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
619
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontWeight: 600, fontSize: 15 }, children: title }),
|
|
620
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 12, opacity: 0.85 }, children: isStreaming ? "Escribiendo..." : "En l\xEDnea" })
|
|
621
|
+
] })
|
|
622
|
+
] }),
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
624
|
+
"button",
|
|
625
|
+
{
|
|
626
|
+
onClick: () => setOpen(false),
|
|
627
|
+
"aria-label": "Cerrar chat",
|
|
628
|
+
style: {
|
|
629
|
+
background: "none",
|
|
630
|
+
border: "none",
|
|
631
|
+
color: "#ffffff",
|
|
632
|
+
cursor: "pointer",
|
|
633
|
+
padding: 4,
|
|
634
|
+
borderRadius: 8,
|
|
635
|
+
display: "flex",
|
|
636
|
+
alignItems: "center",
|
|
637
|
+
justifyContent: "center",
|
|
638
|
+
opacity: 0.85,
|
|
639
|
+
transition: "opacity 0.15s ease"
|
|
640
|
+
},
|
|
641
|
+
onMouseEnter: (e) => {
|
|
642
|
+
e.currentTarget.style.opacity = "1";
|
|
643
|
+
},
|
|
644
|
+
onMouseLeave: (e) => {
|
|
645
|
+
e.currentTarget.style.opacity = "0.85";
|
|
646
|
+
},
|
|
647
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
648
|
+
"svg",
|
|
649
|
+
{
|
|
650
|
+
width: "20",
|
|
651
|
+
height: "20",
|
|
652
|
+
viewBox: "0 0 24 24",
|
|
653
|
+
fill: "none",
|
|
654
|
+
stroke: "currentColor",
|
|
655
|
+
strokeWidth: "2.5",
|
|
656
|
+
strokeLinecap: "round",
|
|
657
|
+
strokeLinejoin: "round",
|
|
658
|
+
children: [
|
|
659
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
660
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
661
|
+
]
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
),
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MessageList, { messages, primaryColor }),
|
|
670
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
671
|
+
InputBar,
|
|
672
|
+
{
|
|
673
|
+
onSend: handleSend,
|
|
674
|
+
isStreaming,
|
|
675
|
+
placeholder,
|
|
676
|
+
primaryColor
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
]
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/AIWidget.tsx
|
|
685
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
686
|
+
function AIWidget(props) {
|
|
687
|
+
var _a, _b, _c;
|
|
688
|
+
const { isOpen, setOpen } = useChatStore();
|
|
689
|
+
const primaryColor = (_a = props.primaryColor) != null ? _a : "#6366f1";
|
|
690
|
+
const position = (_b = props.position) != null ? _b : "bottom-right";
|
|
691
|
+
const zIndex = (_c = props.zIndex) != null ? _c : 9999;
|
|
692
|
+
(0, import_react4.useEffect)(() => {
|
|
693
|
+
if (props.initialOpen) {
|
|
694
|
+
setOpen(true);
|
|
695
|
+
}
|
|
696
|
+
}, []);
|
|
697
|
+
function handleBubbleClick() {
|
|
698
|
+
setOpen(!isOpen);
|
|
699
|
+
}
|
|
700
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
701
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
702
|
+
ChatBubble,
|
|
703
|
+
{
|
|
704
|
+
isOpen,
|
|
705
|
+
onClick: handleBubbleClick,
|
|
706
|
+
primaryColor,
|
|
707
|
+
position,
|
|
708
|
+
zIndex
|
|
709
|
+
}
|
|
710
|
+
),
|
|
711
|
+
isOpen && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChatWindow, { config: props })
|
|
712
|
+
] });
|
|
713
|
+
}
|
|
714
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
715
|
+
0 && (module.exports = {
|
|
716
|
+
AIWidget
|
|
717
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kayro-ia/widget",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Embeddable AI chat widget for your product — powered by Kairo",
|
|
5
|
+
"author": "Kairo <hola@kayro.com.ar>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://kayro.com.ar",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/kairo-ia/helix-monorepo"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kairo-ia/helix-monorepo/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ai", "chatbot", "widget", "mcp", "kairo", "react", "chat", "llm", "streaming"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsup src/index.tsx --watch",
|
|
24
|
+
"build": "tsup src/index.tsx --dts",
|
|
25
|
+
"type-check": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18.0.0",
|
|
29
|
+
"react-dom": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"zustand": "^5.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.0.0",
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.4.0"
|
|
38
|
+
}
|
|
39
|
+
}
|