@dgrtechlabs/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.
- package/README.md +107 -0
- package/dist/aichat-widget.js +423 -0
- package/dist/aichat-widget.js.map +1 -0
- package/dist/embed.d.ts +1 -0
- package/dist/embed.js +213 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/sse.d.ts +16 -0
- package/dist/styles.d.ts +1 -0
- package/dist/widget.d.ts +32 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @dgrtechlabs/chat-widget
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@dgrtechlabs/chat-widget)
|
|
4
|
+
|
|
5
|
+
Embeddable, framework-agnostic **AI chat widget**. It's a single `<ai-chat-widget>`
|
|
6
|
+
[custom element](https://developer.mozilla.org/docs/Web/API/Web_components) that renders
|
|
7
|
+
a floating launcher + chat panel inside a **Shadow DOM** — so the host page's CSS/JS and
|
|
8
|
+
the widget's are fully isolated from each other — and streams replies token-by-token over
|
|
9
|
+
SSE from your chat server.
|
|
10
|
+
|
|
11
|
+
- **Zero runtime dependencies**, ~4 kB gzipped.
|
|
12
|
+
- **Two ways to embed:** a one-line `<script>` tag (any site) or an ESM `import` (frameworks).
|
|
13
|
+
- **No secrets in the page** — the widget only carries a tenant *public* key; provider API
|
|
14
|
+
keys live on the server.
|
|
15
|
+
|
|
16
|
+
The widget talks to a companion chat server (`POST /v1/chat`, SSE). You point it at your
|
|
17
|
+
server with `server-url` and identify the tenant with `public-key`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Option A — drop-in `<script>` from a CDN (any site)
|
|
22
|
+
|
|
23
|
+
Paste one tag. `embed.js` reads its own `data-*` attributes and injects the widget into
|
|
24
|
+
`<body>` — nothing else to wire up. Served straight from npm via
|
|
25
|
+
[jsDelivr](https://www.jsdelivr.com/):
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script
|
|
29
|
+
src="https://cdn.jsdelivr.net/npm/@dgrtechlabs/chat-widget@0.1.0/dist/embed.js"
|
|
30
|
+
data-public-key="pk_live_acme_2f9c"
|
|
31
|
+
data-server-url="https://chat.example.com"
|
|
32
|
+
data-title="Acme Assistant"
|
|
33
|
+
data-accent="#6c5ce7"></script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Pin a version (`@0.1.0`) in production for a stable, immutable, cached URL. `@0.1` tracks
|
|
37
|
+
patch releases; omitting the version tracks `@latest`.
|
|
38
|
+
- [unpkg](https://unpkg.com) works too:
|
|
39
|
+
`https://unpkg.com/@dgrtechlabs/chat-widget@0.1.0/dist/embed.js`.
|
|
40
|
+
|
|
41
|
+
## Option B — ESM import (Astro, Next, Vue, Svelte, plain bundlers)
|
|
42
|
+
|
|
43
|
+
Install, import the module once (that registers the `<ai-chat-widget>` element), then place
|
|
44
|
+
the element in your markup:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @dgrtechlabs/chat-widget
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import '@dgrtechlabs/chat-widget'; // registers <ai-chat-widget>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<ai-chat-widget
|
|
56
|
+
public-key="pk_live_acme_2f9c"
|
|
57
|
+
server-url="https://chat.example.com"
|
|
58
|
+
title="Acme Assistant"
|
|
59
|
+
accent="#0ea5e9"></ai-chat-widget>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The import is side-effecting (it defines the element), so keep it even if you don't
|
|
63
|
+
reference any named exports.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
Both paths share the same options — `data-*` attributes on the `<script>` for the CDN path,
|
|
70
|
+
plain attributes on `<ai-chat-widget>` for the ESM path.
|
|
71
|
+
|
|
72
|
+
| `<script>` (CDN) | `<ai-chat-widget>` (ESM) | Required | Description |
|
|
73
|
+
| ------------------ | ------------------------ | :------: | ------------------------------------------------------------------ |
|
|
74
|
+
| `data-public-key` | `public-key` | yes | Tenant public key; safe to expose in the browser. |
|
|
75
|
+
| `data-server-url` | `server-url` | yes | Base URL of your chat server. The widget POSTs to `…/v1/chat`. |
|
|
76
|
+
| `data-title` | `title` | no | Header text. Defaults to `Chat`. |
|
|
77
|
+
| `data-accent` | `accent` | no | Accent color (any CSS color). Defaults to `#2563eb`. |
|
|
78
|
+
|
|
79
|
+
> The page's origin (`scheme://host:port`) must be in the tenant's `allowedOrigins` on the
|
|
80
|
+
> server, or requests are rejected with **403**. See the server onboarding guide.
|
|
81
|
+
|
|
82
|
+
## Exports
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { AIChatWidget, TAG, CONTRACT_VERSION } from '@dgrtechlabs/chat-widget';
|
|
86
|
+
// ^ the HTMLElement subclass ^ 'ai-chat-widget' ^ wire-contract version
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
TypeScript declarations (`.d.ts`) ship in the package; the type surface is self-contained
|
|
90
|
+
(no extra `@types/*` needed).
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
The widget sends the full message history on each turn (`{ publicKey, messages,
|
|
95
|
+
conversationId }`) and reads the server's `text/event-stream` response, appending each
|
|
96
|
+
`delta` to the live assistant bubble and finalizing on `done`. There is no client-side
|
|
97
|
+
persistence — history lives in the open panel for the session.
|
|
98
|
+
|
|
99
|
+
## Links
|
|
100
|
+
|
|
101
|
+
- **Embedding guide (end-to-end):** [`docs/embedding.md`](https://github.com/ndogar87/aichatbot/blob/main/docs/embedding.md)
|
|
102
|
+
- **Tenant / server onboarding:** [`docs/onboarding.md`](https://github.com/ndogar87/aichatbot/blob/main/docs/onboarding.md)
|
|
103
|
+
- **Source:** [github.com/ndogar87/aichatbot](https://github.com/ndogar87/aichatbot/tree/main/packages/widget)
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
var x = Object.defineProperty;
|
|
2
|
+
var v = (r, o, e) => o in r ? x(r, o, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[o] = e;
|
|
3
|
+
var a = (r, o, e) => v(r, typeof o != "symbol" ? o + "" : o, e);
|
|
4
|
+
var y = "1", w = "/v1/chat";
|
|
5
|
+
const k = (
|
|
6
|
+
/* css */
|
|
7
|
+
`
|
|
8
|
+
:host {
|
|
9
|
+
all: initial;
|
|
10
|
+
--accent: #2563eb;
|
|
11
|
+
--accent-contrast: #ffffff;
|
|
12
|
+
--bg: #ffffff;
|
|
13
|
+
--fg: #1f2329;
|
|
14
|
+
--muted: #6b7280;
|
|
15
|
+
--bubble-assistant: #f1f3f5;
|
|
16
|
+
--radius: 16px;
|
|
17
|
+
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
|
|
18
|
+
sans-serif;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
|
|
23
|
+
.launcher {
|
|
24
|
+
position: fixed;
|
|
25
|
+
bottom: 20px;
|
|
26
|
+
right: 20px;
|
|
27
|
+
width: 56px;
|
|
28
|
+
height: 56px;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
border: none;
|
|
31
|
+
background: var(--accent);
|
|
32
|
+
color: var(--accent-contrast);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
|
|
38
|
+
z-index: 2147483000;
|
|
39
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
40
|
+
}
|
|
41
|
+
.launcher:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.22); }
|
|
42
|
+
.launcher:active { transform: translateY(0); }
|
|
43
|
+
.launcher svg { width: 26px; height: 26px; }
|
|
44
|
+
|
|
45
|
+
.panel {
|
|
46
|
+
position: fixed;
|
|
47
|
+
bottom: 88px;
|
|
48
|
+
right: 20px;
|
|
49
|
+
width: 370px;
|
|
50
|
+
max-width: calc(100vw - 40px);
|
|
51
|
+
height: 540px;
|
|
52
|
+
max-height: calc(100vh - 120px);
|
|
53
|
+
background: var(--bg);
|
|
54
|
+
color: var(--fg);
|
|
55
|
+
font-family: var(--font);
|
|
56
|
+
font-size: 14px;
|
|
57
|
+
line-height: 1.5;
|
|
58
|
+
border-radius: var(--radius);
|
|
59
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.24);
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
z-index: 2147483000;
|
|
64
|
+
opacity: 0;
|
|
65
|
+
transform: translateY(12px) scale(0.98);
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
68
|
+
}
|
|
69
|
+
.panel.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
|
|
70
|
+
|
|
71
|
+
.header {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 8px;
|
|
75
|
+
padding: 14px 16px;
|
|
76
|
+
background: var(--accent);
|
|
77
|
+
color: var(--accent-contrast);
|
|
78
|
+
}
|
|
79
|
+
.header .title { font-weight: 600; font-size: 15px; flex: 1; }
|
|
80
|
+
.header .close {
|
|
81
|
+
border: none;
|
|
82
|
+
background: transparent;
|
|
83
|
+
color: inherit;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
font-size: 20px;
|
|
86
|
+
line-height: 1;
|
|
87
|
+
padding: 2px 6px;
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
opacity: 0.85;
|
|
90
|
+
}
|
|
91
|
+
.header .close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }
|
|
92
|
+
|
|
93
|
+
.messages {
|
|
94
|
+
flex: 1;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
padding: 16px;
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
gap: 10px;
|
|
100
|
+
}
|
|
101
|
+
.messages:empty::before {
|
|
102
|
+
content: attr(data-empty);
|
|
103
|
+
color: var(--muted);
|
|
104
|
+
margin: auto;
|
|
105
|
+
text-align: center;
|
|
106
|
+
padding: 0 24px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.msg {
|
|
110
|
+
max-width: 82%;
|
|
111
|
+
padding: 9px 13px;
|
|
112
|
+
border-radius: 14px;
|
|
113
|
+
white-space: pre-wrap;
|
|
114
|
+
word-wrap: break-word;
|
|
115
|
+
overflow-wrap: anywhere;
|
|
116
|
+
}
|
|
117
|
+
.msg.user {
|
|
118
|
+
align-self: flex-end;
|
|
119
|
+
background: var(--accent);
|
|
120
|
+
color: var(--accent-contrast);
|
|
121
|
+
border-bottom-right-radius: 4px;
|
|
122
|
+
}
|
|
123
|
+
.msg.assistant {
|
|
124
|
+
align-self: flex-start;
|
|
125
|
+
background: var(--bubble-assistant);
|
|
126
|
+
color: var(--fg);
|
|
127
|
+
border-bottom-left-radius: 4px;
|
|
128
|
+
}
|
|
129
|
+
.msg.error { background: #fde8e8; color: #b42318; }
|
|
130
|
+
.msg.pending::after {
|
|
131
|
+
content: '▋';
|
|
132
|
+
margin-left: 1px;
|
|
133
|
+
animation: blink 1s steps(2, start) infinite;
|
|
134
|
+
}
|
|
135
|
+
@keyframes blink { to { visibility: hidden; } }
|
|
136
|
+
|
|
137
|
+
.composer {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: flex-end;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
padding: 10px;
|
|
142
|
+
border-top: 1px solid #eceef1;
|
|
143
|
+
}
|
|
144
|
+
.composer textarea {
|
|
145
|
+
flex: 1;
|
|
146
|
+
resize: none;
|
|
147
|
+
border: 1px solid #d7dade;
|
|
148
|
+
border-radius: 12px;
|
|
149
|
+
padding: 9px 12px;
|
|
150
|
+
font-family: inherit;
|
|
151
|
+
font-size: 14px;
|
|
152
|
+
line-height: 1.4;
|
|
153
|
+
max-height: 120px;
|
|
154
|
+
outline: none;
|
|
155
|
+
background: var(--bg);
|
|
156
|
+
color: var(--fg);
|
|
157
|
+
}
|
|
158
|
+
.composer textarea:focus { border-color: var(--accent); }
|
|
159
|
+
.composer .send {
|
|
160
|
+
border: none;
|
|
161
|
+
background: var(--accent);
|
|
162
|
+
color: var(--accent-contrast);
|
|
163
|
+
width: 40px;
|
|
164
|
+
height: 40px;
|
|
165
|
+
border-radius: 12px;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
flex: none;
|
|
171
|
+
}
|
|
172
|
+
.composer .send:disabled { opacity: 0.45; cursor: default; }
|
|
173
|
+
.composer .send svg { width: 20px; height: 20px; }
|
|
174
|
+
|
|
175
|
+
@media (max-width: 480px) {
|
|
176
|
+
.panel {
|
|
177
|
+
right: 10px;
|
|
178
|
+
left: 10px;
|
|
179
|
+
bottom: 10px;
|
|
180
|
+
width: auto;
|
|
181
|
+
height: auto;
|
|
182
|
+
top: 10px;
|
|
183
|
+
max-height: none;
|
|
184
|
+
}
|
|
185
|
+
.launcher { bottom: 16px; right: 16px; }
|
|
186
|
+
}
|
|
187
|
+
`
|
|
188
|
+
);
|
|
189
|
+
async function S(r, o, e, s) {
|
|
190
|
+
const t = r.replace(/\/+$/, "") + w;
|
|
191
|
+
let n;
|
|
192
|
+
try {
|
|
193
|
+
n = await fetch(t, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
"content-type": "application/json",
|
|
197
|
+
accept: "text/event-stream"
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify(o),
|
|
200
|
+
signal: s
|
|
201
|
+
});
|
|
202
|
+
} catch (l) {
|
|
203
|
+
if (f(l)) return;
|
|
204
|
+
e.onError("network", l instanceof Error ? l.message : "Request failed");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!n.ok || !n.body) {
|
|
208
|
+
e.onError(
|
|
209
|
+
String(n.status || "no_body"),
|
|
210
|
+
n.statusText || "No response stream"
|
|
211
|
+
);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const i = n.body.getReader(), c = new TextDecoder();
|
|
215
|
+
let d = "";
|
|
216
|
+
try {
|
|
217
|
+
for (; ; ) {
|
|
218
|
+
const { value: l, done: b } = await i.read();
|
|
219
|
+
if (b) break;
|
|
220
|
+
d += c.decode(l, { stream: !0 }).replace(/\r/g, "");
|
|
221
|
+
let p;
|
|
222
|
+
for (; (p = d.indexOf(`
|
|
223
|
+
|
|
224
|
+
`)) !== -1; ) {
|
|
225
|
+
const m = d.slice(0, p);
|
|
226
|
+
if (d = d.slice(p + 2), h(m, e)) return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
d.trim() && h(d, e);
|
|
230
|
+
} catch (l) {
|
|
231
|
+
if (f(l)) return;
|
|
232
|
+
e.onError("stream", l instanceof Error ? l.message : "Stream failed");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function h(r, o) {
|
|
236
|
+
const e = [];
|
|
237
|
+
for (const n of r.split(`
|
|
238
|
+
`))
|
|
239
|
+
n.startsWith(":") || n.startsWith("data:") && e.push(n.slice(5).replace(/^ /, ""));
|
|
240
|
+
if (e.length === 0) return !1;
|
|
241
|
+
const s = e.join(`
|
|
242
|
+
`);
|
|
243
|
+
if (!s || s === "[DONE]") return !1;
|
|
244
|
+
let t;
|
|
245
|
+
try {
|
|
246
|
+
t = JSON.parse(s);
|
|
247
|
+
} catch {
|
|
248
|
+
return !1;
|
|
249
|
+
}
|
|
250
|
+
switch (t.type) {
|
|
251
|
+
case "delta":
|
|
252
|
+
return o.onDelta(t.text), !1;
|
|
253
|
+
case "done":
|
|
254
|
+
return o.onDone(t), !0;
|
|
255
|
+
case "error":
|
|
256
|
+
return o.onError(t.code, t.message), !0;
|
|
257
|
+
default:
|
|
258
|
+
return !1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function f(r) {
|
|
262
|
+
return r instanceof Error && r.name === "AbortError";
|
|
263
|
+
}
|
|
264
|
+
const O = y, u = "ai-chat-widget", E = (
|
|
265
|
+
/* html */
|
|
266
|
+
`
|
|
267
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
268
|
+
<path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5Z"
|
|
269
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
270
|
+
</svg>`
|
|
271
|
+
), A = (
|
|
272
|
+
/* html */
|
|
273
|
+
`
|
|
274
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
275
|
+
<path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z"
|
|
276
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
277
|
+
</svg>`
|
|
278
|
+
);
|
|
279
|
+
class C extends HTMLElement {
|
|
280
|
+
constructor() {
|
|
281
|
+
super(...arguments);
|
|
282
|
+
/** Canonical conversation history sent on every request (no server persistence in M1). */
|
|
283
|
+
a(this, "messages", []);
|
|
284
|
+
a(this, "conversationId", L());
|
|
285
|
+
a(this, "open", !1);
|
|
286
|
+
a(this, "streaming", !1);
|
|
287
|
+
a(this, "controller", null);
|
|
288
|
+
// Cached shadow-root nodes, set up once on first connect.
|
|
289
|
+
a(this, "panel");
|
|
290
|
+
a(this, "launcher");
|
|
291
|
+
a(this, "list");
|
|
292
|
+
a(this, "input");
|
|
293
|
+
a(this, "sendBtn");
|
|
294
|
+
a(this, "mounted", !1);
|
|
295
|
+
}
|
|
296
|
+
connectedCallback() {
|
|
297
|
+
this.shadowRoot || this.attachShadow({ mode: "open" }), this.mounted || this.render();
|
|
298
|
+
}
|
|
299
|
+
disconnectedCallback() {
|
|
300
|
+
var e;
|
|
301
|
+
(e = this.controller) == null || e.abort();
|
|
302
|
+
}
|
|
303
|
+
get serverUrl() {
|
|
304
|
+
return this.getAttribute("server-url") ?? "";
|
|
305
|
+
}
|
|
306
|
+
get publicKey() {
|
|
307
|
+
return this.getAttribute("public-key") ?? "";
|
|
308
|
+
}
|
|
309
|
+
render() {
|
|
310
|
+
const e = this.shadowRoot, s = this.getAttribute("accent");
|
|
311
|
+
s && this.style.setProperty("--accent", s);
|
|
312
|
+
const t = this.getAttribute("title") || "Chat";
|
|
313
|
+
e.innerHTML = `
|
|
314
|
+
<style>${k}</style>
|
|
315
|
+
<button class="launcher" type="button" part="launcher" aria-label="Open chat">
|
|
316
|
+
${E}
|
|
317
|
+
</button>
|
|
318
|
+
<section class="panel" role="dialog" aria-label="${T(t)}" aria-modal="false">
|
|
319
|
+
<header class="header">
|
|
320
|
+
<span class="title">${g(t)}</span>
|
|
321
|
+
<button class="close" type="button" aria-label="Close chat">×</button>
|
|
322
|
+
</header>
|
|
323
|
+
<div class="messages" role="log" aria-live="polite"
|
|
324
|
+
data-empty="Send a message to start the conversation."></div>
|
|
325
|
+
<form class="composer">
|
|
326
|
+
<textarea rows="1" placeholder="Type a message…" aria-label="Message"></textarea>
|
|
327
|
+
<button class="send" type="submit" aria-label="Send message" disabled>${A}</button>
|
|
328
|
+
</form>
|
|
329
|
+
</section>
|
|
330
|
+
`, this.launcher = e.querySelector(".launcher"), this.panel = e.querySelector(".panel"), this.list = e.querySelector(".messages"), this.input = e.querySelector("textarea"), this.sendBtn = e.querySelector(".send");
|
|
331
|
+
const n = e.querySelector(".composer"), i = e.querySelector(".close");
|
|
332
|
+
this.launcher.addEventListener("click", () => this.toggle(!0)), i.addEventListener("click", () => this.toggle(!1)), n.addEventListener("submit", (c) => {
|
|
333
|
+
c.preventDefault(), this.send();
|
|
334
|
+
}), this.input.addEventListener("input", () => {
|
|
335
|
+
this.autosize(), this.refreshSendState();
|
|
336
|
+
}), this.input.addEventListener("keydown", (c) => {
|
|
337
|
+
c.key === "Enter" && !c.shiftKey && (c.preventDefault(), this.send());
|
|
338
|
+
}), this.mounted = !0;
|
|
339
|
+
}
|
|
340
|
+
toggle(e) {
|
|
341
|
+
var s, t, n;
|
|
342
|
+
this.open = e, (s = this.panel) == null || s.classList.toggle("open", e), (t = this.launcher) == null || t.setAttribute("aria-label", e ? "Close chat" : "Open chat"), e && ((n = this.input) == null || n.focus());
|
|
343
|
+
}
|
|
344
|
+
refreshSendState() {
|
|
345
|
+
const e = !this.input || this.input.value.trim().length === 0;
|
|
346
|
+
this.sendBtn && (this.sendBtn.disabled = e || this.streaming);
|
|
347
|
+
}
|
|
348
|
+
autosize() {
|
|
349
|
+
const e = this.input;
|
|
350
|
+
e && (e.style.height = "auto", e.style.height = Math.min(e.scrollHeight, 120) + "px");
|
|
351
|
+
}
|
|
352
|
+
appendBubble(e, s) {
|
|
353
|
+
const t = document.createElement("div");
|
|
354
|
+
return t.className = `msg ${e}`, t.textContent = s, this.list.appendChild(t), this.scrollToBottom(), t;
|
|
355
|
+
}
|
|
356
|
+
scrollToBottom() {
|
|
357
|
+
this.list && (this.list.scrollTop = this.list.scrollHeight);
|
|
358
|
+
}
|
|
359
|
+
async send() {
|
|
360
|
+
if (this.streaming || !this.input) return;
|
|
361
|
+
const e = this.input.value.trim();
|
|
362
|
+
if (!e) return;
|
|
363
|
+
if (!this.serverUrl || !this.publicKey) {
|
|
364
|
+
this.appendBubble("assistant", "⚠ Widget misconfigured: missing server-url or public-key.").classList.add("error");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.input.value = "", this.autosize(), this.messages.push({ role: "user", content: e }), this.appendBubble("user", e);
|
|
368
|
+
const s = this.appendBubble("assistant", "");
|
|
369
|
+
s.classList.add("pending"), this.streaming = !0, this.refreshSendState(), this.controller = new AbortController();
|
|
370
|
+
let t = "";
|
|
371
|
+
await S(
|
|
372
|
+
this.serverUrl,
|
|
373
|
+
{ publicKey: this.publicKey, messages: this.messages, conversationId: this.conversationId },
|
|
374
|
+
{
|
|
375
|
+
onDelta: (n) => {
|
|
376
|
+
t += n, s.textContent = t, this.scrollToBottom();
|
|
377
|
+
},
|
|
378
|
+
onDone: () => {
|
|
379
|
+
this.messages.push({ role: "assistant", content: t });
|
|
380
|
+
},
|
|
381
|
+
onError: (n, i) => {
|
|
382
|
+
s.classList.add("error"), s.textContent = t ? `${t}
|
|
383
|
+
|
|
384
|
+
⚠ ${i}` : `⚠ ${i} (${n})`, this.scrollToBottom();
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
this.controller.signal
|
|
388
|
+
), s.classList.remove("pending"), this.streaming = !1, this.controller = null, this.refreshSendState(), this.input.focus();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function g(r) {
|
|
392
|
+
return r.replace(/[&<>]/g, (o) => o === "&" ? "&" : o === "<" ? "<" : ">");
|
|
393
|
+
}
|
|
394
|
+
function T(r) {
|
|
395
|
+
return g(r).replace(/"/g, """);
|
|
396
|
+
}
|
|
397
|
+
function L() {
|
|
398
|
+
const r = globalThis.crypto;
|
|
399
|
+
return r && typeof r.randomUUID == "function" ? r.randomUUID() : "c-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
400
|
+
}
|
|
401
|
+
typeof customElements < "u" && !customElements.get(u) && customElements.define(u, C);
|
|
402
|
+
function B() {
|
|
403
|
+
if (typeof document > "u") return;
|
|
404
|
+
const r = document.currentScript;
|
|
405
|
+
if (!r) return;
|
|
406
|
+
const o = r.getAttribute("data-public-key"), e = r.getAttribute("data-server-url");
|
|
407
|
+
if (!o || !e || document.querySelector(u)) return;
|
|
408
|
+
const s = document.createElement(u);
|
|
409
|
+
s.setAttribute("public-key", o), s.setAttribute("server-url", e);
|
|
410
|
+
const t = r.getAttribute("data-title"), n = r.getAttribute("data-accent");
|
|
411
|
+
t && s.setAttribute("title", t), n && s.setAttribute("accent", n);
|
|
412
|
+
const i = () => {
|
|
413
|
+
document.body.appendChild(s);
|
|
414
|
+
};
|
|
415
|
+
document.body ? i() : document.addEventListener("DOMContentLoaded", i, { once: !0 });
|
|
416
|
+
}
|
|
417
|
+
B();
|
|
418
|
+
export {
|
|
419
|
+
C as AIChatWidget,
|
|
420
|
+
O as CONTRACT_VERSION,
|
|
421
|
+
u as TAG
|
|
422
|
+
};
|
|
423
|
+
//# sourceMappingURL=aichat-widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aichat-widget.js","sources":["../../shared/dist/index.js","../src/styles.ts","../src/sse.ts","../src/widget.ts","../src/embed.ts","../src/index.ts"],"sourcesContent":["// src/index.ts\nvar CONTRACT_VERSION = \"1\";\nvar CHAT_ENDPOINT = \"/v1/chat\";\nvar PROVIDER_IDS = [\"anthropic\", \"openai\", \"gemini\", \"openrouter\"];\nexport {\n CHAT_ENDPOINT,\n CONTRACT_VERSION,\n PROVIDER_IDS\n};\n","// All widget CSS, scoped inside the shadow root.\n//\n// `:host { all: initial }` resets every inherited property at the boundary so the\n// host page's font/color/box-sizing can't leak in; CSS custom properties (our\n// `--accent`) are unaffected by `all` and pass through. Nothing here escapes the\n// shadow root, so the page's styles are untouched too.\n\nexport const STYLES = /* css */ `\n:host {\n all: initial;\n --accent: #2563eb;\n --accent-contrast: #ffffff;\n --bg: #ffffff;\n --fg: #1f2329;\n --muted: #6b7280;\n --bubble-assistant: #f1f3f5;\n --radius: 16px;\n --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,\n sans-serif;\n}\n\n* { box-sizing: border-box; }\n\n.launcher {\n position: fixed;\n bottom: 20px;\n right: 20px;\n width: 56px;\n height: 56px;\n border-radius: 50%;\n border: none;\n background: var(--accent);\n color: var(--accent-contrast);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);\n z-index: 2147483000;\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n.launcher:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.22); }\n.launcher:active { transform: translateY(0); }\n.launcher svg { width: 26px; height: 26px; }\n\n.panel {\n position: fixed;\n bottom: 88px;\n right: 20px;\n width: 370px;\n max-width: calc(100vw - 40px);\n height: 540px;\n max-height: calc(100vh - 120px);\n background: var(--bg);\n color: var(--fg);\n font-family: var(--font);\n font-size: 14px;\n line-height: 1.5;\n border-radius: var(--radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.24);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n z-index: 2147483000;\n opacity: 0;\n transform: translateY(12px) scale(0.98);\n pointer-events: none;\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.panel.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }\n\n.header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 14px 16px;\n background: var(--accent);\n color: var(--accent-contrast);\n}\n.header .title { font-weight: 600; font-size: 15px; flex: 1; }\n.header .close {\n border: none;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font-size: 20px;\n line-height: 1;\n padding: 2px 6px;\n border-radius: 8px;\n opacity: 0.85;\n}\n.header .close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.messages:empty::before {\n content: attr(data-empty);\n color: var(--muted);\n margin: auto;\n text-align: center;\n padding: 0 24px;\n}\n\n.msg {\n max-width: 82%;\n padding: 9px 13px;\n border-radius: 14px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: anywhere;\n}\n.msg.user {\n align-self: flex-end;\n background: var(--accent);\n color: var(--accent-contrast);\n border-bottom-right-radius: 4px;\n}\n.msg.assistant {\n align-self: flex-start;\n background: var(--bubble-assistant);\n color: var(--fg);\n border-bottom-left-radius: 4px;\n}\n.msg.error { background: #fde8e8; color: #b42318; }\n.msg.pending::after {\n content: '▋';\n margin-left: 1px;\n animation: blink 1s steps(2, start) infinite;\n}\n@keyframes blink { to { visibility: hidden; } }\n\n.composer {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 10px;\n border-top: 1px solid #eceef1;\n}\n.composer textarea {\n flex: 1;\n resize: none;\n border: 1px solid #d7dade;\n border-radius: 12px;\n padding: 9px 12px;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n max-height: 120px;\n outline: none;\n background: var(--bg);\n color: var(--fg);\n}\n.composer textarea:focus { border-color: var(--accent); }\n.composer .send {\n border: none;\n background: var(--accent);\n color: var(--accent-contrast);\n width: 40px;\n height: 40px;\n border-radius: 12px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n.composer .send:disabled { opacity: 0.45; cursor: default; }\n.composer .send svg { width: 20px; height: 20px; }\n\n@media (max-width: 480px) {\n .panel {\n right: 10px;\n left: 10px;\n bottom: 10px;\n width: auto;\n height: auto;\n top: 10px;\n max-height: none;\n }\n .launcher { bottom: 16px; right: 16px; }\n}\n`;\n","// SSE chat client (WS3).\n//\n// POSTs a ChatRequest to `{serverUrl}{CHAT_ENDPOINT}` and reads the server's\n// text/event-stream response via fetch + ReadableStream — no EventSource (which\n// can't POST). Each SSE event's `data:` payload is one JSON ChatStreamEvent.\n\nimport { CHAT_ENDPOINT } from '@aichat/shared';\nimport type { ChatRequest, ChatStreamEvent } from '@aichat/shared';\n\nexport interface StreamHandlers {\n /** Incremental assistant text. */\n onDelta(text: string): void;\n /** Stream finished cleanly. */\n onDone(event: Extract<ChatStreamEvent, { type: 'done' }>): void;\n /** Transport or server-reported error; terminal. */\n onError(code: string, message: string): void;\n}\n\n/**\n * Stream one completion. Resolves when the stream ends (success or error);\n * all outcomes are reported through `handlers`, never thrown.\n */\nexport async function streamChat(\n serverUrl: string,\n request: ChatRequest,\n handlers: StreamHandlers,\n signal?: AbortSignal,\n): Promise<void> {\n const url = serverUrl.replace(/\\/+$/, '') + CHAT_ENDPOINT;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n },\n body: JSON.stringify(request),\n signal,\n });\n } catch (err) {\n if (isAbort(err)) return;\n handlers.onError('network', err instanceof Error ? err.message : 'Request failed');\n return;\n }\n\n if (!res.ok || !res.body) {\n handlers.onError(\n String(res.status || 'no_body'),\n res.statusText || 'No response stream',\n );\n return;\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n // Strip CRs so events split cleanly whether the server uses \\n or \\r\\n.\n buffer += decoder.decode(value, { stream: true }).replace(/\\r/g, '');\n\n let sep: number;\n while ((sep = buffer.indexOf('\\n\\n')) !== -1) {\n const rawEvent = buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n if (dispatch(rawEvent, handlers)) return; // terminal event seen\n }\n }\n // Flush a trailing event that lacked the terminating blank line.\n if (buffer.trim()) dispatch(buffer, handlers);\n } catch (err) {\n if (isAbort(err)) return;\n handlers.onError('stream', err instanceof Error ? err.message : 'Stream failed');\n }\n}\n\n/** Parse one SSE event block. Returns true if it was a terminal event. */\nfunction dispatch(rawEvent: string, handlers: StreamHandlers): boolean {\n const dataLines: string[] = [];\n for (const line of rawEvent.split('\\n')) {\n if (line.startsWith(':')) continue; // comment / keep-alive\n if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return false;\n\n const payload = dataLines.join('\\n');\n if (!payload || payload === '[DONE]') return false;\n\n let event: ChatStreamEvent;\n try {\n event = JSON.parse(payload) as ChatStreamEvent;\n } catch {\n return false; // ignore unparseable lines\n }\n\n switch (event.type) {\n case 'delta':\n handlers.onDelta(event.text);\n return false;\n case 'done':\n handlers.onDone(event);\n return true;\n case 'error':\n handlers.onError(event.code, event.message);\n return true;\n default:\n return false;\n }\n}\n\nfunction isAbort(err: unknown): boolean {\n return err instanceof Error && err.name === 'AbortError';\n}\n","// <ai-chat-widget> — the framework-agnostic chat web component (WS3).\n//\n// Renders a floating launcher + chat panel entirely inside a Shadow DOM so the\n// host page's CSS/JS and the widget's are mutually isolated. Configuration comes\n// from attributes: `public-key`, `server-url` (required) and `title`, `accent`.\n\nimport { CONTRACT_VERSION as WIRE_CONTRACT_VERSION } from '@aichat/shared';\nimport type { ChatMessage } from '@aichat/shared';\n\n/**\n * Wire-contract version this widget speaks. Re-assigned to a local const (rather\n * than re-exported) so the emitted .d.ts inlines the literal and the published\n * type surface stays self-contained — no shared-package import for consumers.\n */\nexport const CONTRACT_VERSION = WIRE_CONTRACT_VERSION;\nimport { STYLES } from './styles';\nimport { streamChat } from './sse';\n\nexport const TAG = 'ai-chat-widget';\n\nconst LAUNCHER_ICON = /* html */ `\n<svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5Z\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\nconst SEND_ICON = /* html */ `\n<svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\nexport class AIChatWidget extends HTMLElement {\n /** Canonical conversation history sent on every request (no server persistence in M1). */\n private messages: ChatMessage[] = [];\n private conversationId = generateId();\n private open = false;\n private streaming = false;\n private controller: AbortController | null = null;\n\n // Cached shadow-root nodes, set up once on first connect.\n private panel?: HTMLElement;\n private launcher?: HTMLButtonElement;\n private list?: HTMLElement;\n private input?: HTMLTextAreaElement;\n private sendBtn?: HTMLButtonElement;\n private mounted = false;\n\n connectedCallback(): void {\n if (!this.shadowRoot) this.attachShadow({ mode: 'open' });\n if (!this.mounted) this.render();\n }\n\n disconnectedCallback(): void {\n this.controller?.abort();\n }\n\n private get serverUrl(): string {\n return this.getAttribute('server-url') ?? '';\n }\n private get publicKey(): string {\n return this.getAttribute('public-key') ?? '';\n }\n\n private render(): void {\n const root = this.shadowRoot!;\n const accent = this.getAttribute('accent');\n if (accent) this.style.setProperty('--accent', accent); // custom prop passes the shadow boundary\n const title = this.getAttribute('title') || 'Chat';\n\n root.innerHTML = `\n <style>${STYLES}</style>\n <button class=\"launcher\" type=\"button\" part=\"launcher\" aria-label=\"Open chat\">\n ${LAUNCHER_ICON}\n </button>\n <section class=\"panel\" role=\"dialog\" aria-label=\"${escapeAttr(title)}\" aria-modal=\"false\">\n <header class=\"header\">\n <span class=\"title\">${escapeHtml(title)}</span>\n <button class=\"close\" type=\"button\" aria-label=\"Close chat\">×</button>\n </header>\n <div class=\"messages\" role=\"log\" aria-live=\"polite\"\n data-empty=\"Send a message to start the conversation.\"></div>\n <form class=\"composer\">\n <textarea rows=\"1\" placeholder=\"Type a message…\" aria-label=\"Message\"></textarea>\n <button class=\"send\" type=\"submit\" aria-label=\"Send message\" disabled>${SEND_ICON}</button>\n </form>\n </section>\n `;\n\n this.launcher = root.querySelector('.launcher')!;\n this.panel = root.querySelector('.panel')!;\n this.list = root.querySelector('.messages')!;\n this.input = root.querySelector('textarea')!;\n this.sendBtn = root.querySelector('.send')!;\n const form = root.querySelector('.composer') as HTMLFormElement;\n const close = root.querySelector('.close') as HTMLButtonElement;\n\n this.launcher.addEventListener('click', () => this.toggle(true));\n close.addEventListener('click', () => this.toggle(false));\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n void this.send();\n });\n\n this.input.addEventListener('input', () => {\n this.autosize();\n this.refreshSendState();\n });\n this.input.addEventListener('keydown', (e) => {\n // Enter sends; Shift+Enter inserts a newline.\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n void this.send();\n }\n });\n\n this.mounted = true;\n }\n\n private toggle(open: boolean): void {\n this.open = open;\n this.panel?.classList.toggle('open', open);\n this.launcher?.setAttribute('aria-label', open ? 'Close chat' : 'Open chat');\n if (open) this.input?.focus();\n }\n\n private refreshSendState(): void {\n const empty = !this.input || this.input.value.trim().length === 0;\n if (this.sendBtn) this.sendBtn.disabled = empty || this.streaming;\n }\n\n private autosize(): void {\n const el = this.input;\n if (!el) return;\n el.style.height = 'auto';\n el.style.height = Math.min(el.scrollHeight, 120) + 'px';\n }\n\n private appendBubble(role: ChatMessage['role'], text: string): HTMLElement {\n const bubble = document.createElement('div');\n bubble.className = `msg ${role}`;\n bubble.textContent = text;\n this.list!.appendChild(bubble);\n this.scrollToBottom();\n return bubble;\n }\n\n private scrollToBottom(): void {\n if (this.list) this.list.scrollTop = this.list.scrollHeight;\n }\n\n private async send(): Promise<void> {\n if (this.streaming || !this.input) return;\n const text = this.input.value.trim();\n if (!text) return;\n\n if (!this.serverUrl || !this.publicKey) {\n this.appendBubble('assistant', '⚠ Widget misconfigured: missing server-url or public-key.')\n .classList.add('error');\n return;\n }\n\n // User turn.\n this.input.value = '';\n this.autosize();\n this.messages.push({ role: 'user', content: text });\n this.appendBubble('user', text);\n\n // Assistant turn — stream into a live bubble.\n const bubble = this.appendBubble('assistant', '');\n bubble.classList.add('pending');\n this.streaming = true;\n this.refreshSendState();\n this.controller = new AbortController();\n\n let acc = '';\n await streamChat(\n this.serverUrl,\n { publicKey: this.publicKey, messages: this.messages, conversationId: this.conversationId },\n {\n onDelta: (delta) => {\n acc += delta;\n bubble.textContent = acc;\n this.scrollToBottom();\n },\n onDone: () => {\n this.messages.push({ role: 'assistant', content: acc });\n },\n onError: (code, message) => {\n bubble.classList.add('error');\n bubble.textContent = acc ? `${acc}\\n\\n⚠ ${message}` : `⚠ ${message} (${code})`;\n this.scrollToBottom();\n },\n },\n this.controller.signal,\n );\n\n bubble.classList.remove('pending');\n this.streaming = false;\n this.controller = null;\n this.refreshSendState();\n this.input.focus();\n }\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>]/g, (c) => (c === '&' ? '&' : c === '<' ? '<' : '>'));\n}\nfunction escapeAttr(s: string): string {\n return escapeHtml(s).replace(/\"/g, '"');\n}\n\nfunction generateId(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === 'function') return c.randomUUID();\n return 'c-' + Math.random().toString(36).slice(2) + Date.now().toString(36);\n}\n\n// Register the element on import. Both the ESM and embed builds pull this in.\nif (typeof customElements !== 'undefined' && !customElements.get(TAG)) {\n customElements.define(TAG, AIChatWidget);\n}\n\n","// Auto-inject path for the plain <script> embed (WS3).\n//\n// When the bundle is loaded as `embed.js` via a <script> tag, `document.currentScript`\n// is that tag — we read its data-* attributes and drop an <ai-chat-widget> into the\n// page. When the same bundle is imported as an ES module (framework apps),\n// `document.currentScript` is null, so this is a no-op and the host mounts the\n// element itself.\n\nimport { TAG } from './widget';\n\nexport function autoInject(): void {\n if (typeof document === 'undefined') return;\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return; // ESM import — nothing to auto-mount from.\n\n const publicKey = script.getAttribute('data-public-key');\n const serverUrl = script.getAttribute('data-server-url');\n if (!publicKey || !serverUrl) return; // not configured for auto-embed\n if (document.querySelector(TAG)) return; // already present\n\n const el = document.createElement(TAG);\n el.setAttribute('public-key', publicKey);\n el.setAttribute('server-url', serverUrl);\n const title = script.getAttribute('data-title');\n const accent = script.getAttribute('data-accent');\n if (title) el.setAttribute('title', title);\n if (accent) el.setAttribute('accent', accent);\n\n const mount = (): void => {\n document.body.appendChild(el);\n };\n if (document.body) mount();\n else document.addEventListener('DOMContentLoaded', mount, { once: true });\n}\n","// @dgrtechlabs/chat-widget — framework-agnostic embeddable chat widget (WS3 entry).\n//\n// Single entry built two ways (see vite.config.ts):\n// • ESM `aichat-widget.js` — `import '@dgrtechlabs/chat-widget'` registers <ai-chat-widget>;\n// the host app places the element. autoInject() is a no-op (no currentScript).\n// • IIFE `embed.js` — drop-in <script> tag; autoInject() reads the tag's\n// data-public-key / data-server-url and mounts the widget into <body>.\n\nimport { AIChatWidget, TAG, CONTRACT_VERSION } from './widget';\nimport { autoInject } from './embed';\n\n// Runs synchronously at load so `document.currentScript` is still the embed tag.\nautoInject();\n\nexport { AIChatWidget, TAG, CONTRACT_VERSION };\n"],"names":["CONTRACT_VERSION","CHAT_ENDPOINT","STYLES","streamChat","serverUrl","request","handlers","signal","url","res","err","isAbort","reader","decoder","buffer","value","done","sep","rawEvent","dispatch","dataLines","line","payload","event","WIRE_CONTRACT_VERSION","TAG","LAUNCHER_ICON","SEND_ICON","AIChatWidget","__publicField","generateId","_a","root","accent","title","escapeAttr","escapeHtml","form","close","e","open","_b","_c","empty","el","role","text","bubble","acc","delta","code","message","s","c","autoInject","script","publicKey","mount"],"mappings":"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"}
|
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function autoInject(): void;
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
var AIChatWidget=(function(i){"use strict";var I=Object.defineProperty;var N=(i,l,h)=>l in i?I(i,l,{enumerable:!0,configurable:!0,writable:!0,value:h}):i[l]=h;var o=(i,l,h)=>N(i,typeof l!="symbol"?l+"":l,h);var l="1",h="/v1/chat";const y=`
|
|
2
|
+
:host {
|
|
3
|
+
all: initial;
|
|
4
|
+
--accent: #2563eb;
|
|
5
|
+
--accent-contrast: #ffffff;
|
|
6
|
+
--bg: #ffffff;
|
|
7
|
+
--fg: #1f2329;
|
|
8
|
+
--muted: #6b7280;
|
|
9
|
+
--bubble-assistant: #f1f3f5;
|
|
10
|
+
--radius: 16px;
|
|
11
|
+
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
|
|
12
|
+
sans-serif;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
|
|
17
|
+
.launcher {
|
|
18
|
+
position: fixed;
|
|
19
|
+
bottom: 20px;
|
|
20
|
+
right: 20px;
|
|
21
|
+
width: 56px;
|
|
22
|
+
height: 56px;
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
border: none;
|
|
25
|
+
background: var(--accent);
|
|
26
|
+
color: var(--accent-contrast);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
|
|
32
|
+
z-index: 2147483000;
|
|
33
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
34
|
+
}
|
|
35
|
+
.launcher:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.22); }
|
|
36
|
+
.launcher:active { transform: translateY(0); }
|
|
37
|
+
.launcher svg { width: 26px; height: 26px; }
|
|
38
|
+
|
|
39
|
+
.panel {
|
|
40
|
+
position: fixed;
|
|
41
|
+
bottom: 88px;
|
|
42
|
+
right: 20px;
|
|
43
|
+
width: 370px;
|
|
44
|
+
max-width: calc(100vw - 40px);
|
|
45
|
+
height: 540px;
|
|
46
|
+
max-height: calc(100vh - 120px);
|
|
47
|
+
background: var(--bg);
|
|
48
|
+
color: var(--fg);
|
|
49
|
+
font-family: var(--font);
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
line-height: 1.5;
|
|
52
|
+
border-radius: var(--radius);
|
|
53
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.24);
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
z-index: 2147483000;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transform: translateY(12px) scale(0.98);
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
62
|
+
}
|
|
63
|
+
.panel.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
|
|
64
|
+
|
|
65
|
+
.header {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
padding: 14px 16px;
|
|
70
|
+
background: var(--accent);
|
|
71
|
+
color: var(--accent-contrast);
|
|
72
|
+
}
|
|
73
|
+
.header .title { font-weight: 600; font-size: 15px; flex: 1; }
|
|
74
|
+
.header .close {
|
|
75
|
+
border: none;
|
|
76
|
+
background: transparent;
|
|
77
|
+
color: inherit;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
font-size: 20px;
|
|
80
|
+
line-height: 1;
|
|
81
|
+
padding: 2px 6px;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
opacity: 0.85;
|
|
84
|
+
}
|
|
85
|
+
.header .close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }
|
|
86
|
+
|
|
87
|
+
.messages {
|
|
88
|
+
flex: 1;
|
|
89
|
+
overflow-y: auto;
|
|
90
|
+
padding: 16px;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: 10px;
|
|
94
|
+
}
|
|
95
|
+
.messages:empty::before {
|
|
96
|
+
content: attr(data-empty);
|
|
97
|
+
color: var(--muted);
|
|
98
|
+
margin: auto;
|
|
99
|
+
text-align: center;
|
|
100
|
+
padding: 0 24px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.msg {
|
|
104
|
+
max-width: 82%;
|
|
105
|
+
padding: 9px 13px;
|
|
106
|
+
border-radius: 14px;
|
|
107
|
+
white-space: pre-wrap;
|
|
108
|
+
word-wrap: break-word;
|
|
109
|
+
overflow-wrap: anywhere;
|
|
110
|
+
}
|
|
111
|
+
.msg.user {
|
|
112
|
+
align-self: flex-end;
|
|
113
|
+
background: var(--accent);
|
|
114
|
+
color: var(--accent-contrast);
|
|
115
|
+
border-bottom-right-radius: 4px;
|
|
116
|
+
}
|
|
117
|
+
.msg.assistant {
|
|
118
|
+
align-self: flex-start;
|
|
119
|
+
background: var(--bubble-assistant);
|
|
120
|
+
color: var(--fg);
|
|
121
|
+
border-bottom-left-radius: 4px;
|
|
122
|
+
}
|
|
123
|
+
.msg.error { background: #fde8e8; color: #b42318; }
|
|
124
|
+
.msg.pending::after {
|
|
125
|
+
content: '▋';
|
|
126
|
+
margin-left: 1px;
|
|
127
|
+
animation: blink 1s steps(2, start) infinite;
|
|
128
|
+
}
|
|
129
|
+
@keyframes blink { to { visibility: hidden; } }
|
|
130
|
+
|
|
131
|
+
.composer {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: flex-end;
|
|
134
|
+
gap: 8px;
|
|
135
|
+
padding: 10px;
|
|
136
|
+
border-top: 1px solid #eceef1;
|
|
137
|
+
}
|
|
138
|
+
.composer textarea {
|
|
139
|
+
flex: 1;
|
|
140
|
+
resize: none;
|
|
141
|
+
border: 1px solid #d7dade;
|
|
142
|
+
border-radius: 12px;
|
|
143
|
+
padding: 9px 12px;
|
|
144
|
+
font-family: inherit;
|
|
145
|
+
font-size: 14px;
|
|
146
|
+
line-height: 1.4;
|
|
147
|
+
max-height: 120px;
|
|
148
|
+
outline: none;
|
|
149
|
+
background: var(--bg);
|
|
150
|
+
color: var(--fg);
|
|
151
|
+
}
|
|
152
|
+
.composer textarea:focus { border-color: var(--accent); }
|
|
153
|
+
.composer .send {
|
|
154
|
+
border: none;
|
|
155
|
+
background: var(--accent);
|
|
156
|
+
color: var(--accent-contrast);
|
|
157
|
+
width: 40px;
|
|
158
|
+
height: 40px;
|
|
159
|
+
border-radius: 12px;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
flex: none;
|
|
165
|
+
}
|
|
166
|
+
.composer .send:disabled { opacity: 0.45; cursor: default; }
|
|
167
|
+
.composer .send svg { width: 20px; height: 20px; }
|
|
168
|
+
|
|
169
|
+
@media (max-width: 480px) {
|
|
170
|
+
.panel {
|
|
171
|
+
right: 10px;
|
|
172
|
+
left: 10px;
|
|
173
|
+
bottom: 10px;
|
|
174
|
+
width: auto;
|
|
175
|
+
height: auto;
|
|
176
|
+
top: 10px;
|
|
177
|
+
max-height: none;
|
|
178
|
+
}
|
|
179
|
+
.launcher { bottom: 16px; right: 16px; }
|
|
180
|
+
}
|
|
181
|
+
`;async function w(s,a,e,r){const t=s.replace(/\/+$/,"")+h;let n;try{n=await fetch(t,{method:"POST",headers:{"content-type":"application/json",accept:"text/event-stream"},body:JSON.stringify(a),signal:r})}catch(d){if(m(d))return;e.onError("network",d instanceof Error?d.message:"Request failed");return}if(!n.ok||!n.body){e.onError(String(n.status||"no_body"),n.statusText||"No response stream");return}const c=n.body.getReader(),u=new TextDecoder;let p="";try{for(;;){const{value:d,done:L}=await c.read();if(L)break;p+=u.decode(d,{stream:!0}).replace(/\r/g,"");let g;for(;(g=p.indexOf(`
|
|
182
|
+
|
|
183
|
+
`))!==-1;){const O=p.slice(0,g);if(p=p.slice(g+2),b(O,e))return}}p.trim()&&b(p,e)}catch(d){if(m(d))return;e.onError("stream",d instanceof Error?d.message:"Stream failed")}}function b(s,a){const e=[];for(const n of s.split(`
|
|
184
|
+
`))n.startsWith(":")||n.startsWith("data:")&&e.push(n.slice(5).replace(/^ /,""));if(e.length===0)return!1;const r=e.join(`
|
|
185
|
+
`);if(!r||r==="[DONE]")return!1;let t;try{t=JSON.parse(r)}catch{return!1}switch(t.type){case"delta":return a.onDelta(t.text),!1;case"done":return a.onDone(t),!0;case"error":return a.onError(t.code,t.message),!0;default:return!1}}function m(s){return s instanceof Error&&s.name==="AbortError"}const S=l,f="ai-chat-widget",k=`
|
|
186
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
187
|
+
<path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5Z"
|
|
188
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
189
|
+
</svg>`,E=`
|
|
190
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
191
|
+
<path d="M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z"
|
|
192
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
193
|
+
</svg>`;class x extends HTMLElement{constructor(){super(...arguments);o(this,"messages",[]);o(this,"conversationId",C());o(this,"open",!1);o(this,"streaming",!1);o(this,"controller",null);o(this,"panel");o(this,"launcher");o(this,"list");o(this,"input");o(this,"sendBtn");o(this,"mounted",!1)}connectedCallback(){this.shadowRoot||this.attachShadow({mode:"open"}),this.mounted||this.render()}disconnectedCallback(){var e;(e=this.controller)==null||e.abort()}get serverUrl(){return this.getAttribute("server-url")??""}get publicKey(){return this.getAttribute("public-key")??""}render(){const e=this.shadowRoot,r=this.getAttribute("accent");r&&this.style.setProperty("--accent",r);const t=this.getAttribute("title")||"Chat";e.innerHTML=`
|
|
194
|
+
<style>${y}</style>
|
|
195
|
+
<button class="launcher" type="button" part="launcher" aria-label="Open chat">
|
|
196
|
+
${k}
|
|
197
|
+
</button>
|
|
198
|
+
<section class="panel" role="dialog" aria-label="${A(t)}" aria-modal="false">
|
|
199
|
+
<header class="header">
|
|
200
|
+
<span class="title">${v(t)}</span>
|
|
201
|
+
<button class="close" type="button" aria-label="Close chat">×</button>
|
|
202
|
+
</header>
|
|
203
|
+
<div class="messages" role="log" aria-live="polite"
|
|
204
|
+
data-empty="Send a message to start the conversation."></div>
|
|
205
|
+
<form class="composer">
|
|
206
|
+
<textarea rows="1" placeholder="Type a message…" aria-label="Message"></textarea>
|
|
207
|
+
<button class="send" type="submit" aria-label="Send message" disabled>${E}</button>
|
|
208
|
+
</form>
|
|
209
|
+
</section>
|
|
210
|
+
`,this.launcher=e.querySelector(".launcher"),this.panel=e.querySelector(".panel"),this.list=e.querySelector(".messages"),this.input=e.querySelector("textarea"),this.sendBtn=e.querySelector(".send");const n=e.querySelector(".composer"),c=e.querySelector(".close");this.launcher.addEventListener("click",()=>this.toggle(!0)),c.addEventListener("click",()=>this.toggle(!1)),n.addEventListener("submit",u=>{u.preventDefault(),this.send()}),this.input.addEventListener("input",()=>{this.autosize(),this.refreshSendState()}),this.input.addEventListener("keydown",u=>{u.key==="Enter"&&!u.shiftKey&&(u.preventDefault(),this.send())}),this.mounted=!0}toggle(e){var r,t,n;this.open=e,(r=this.panel)==null||r.classList.toggle("open",e),(t=this.launcher)==null||t.setAttribute("aria-label",e?"Close chat":"Open chat"),e&&((n=this.input)==null||n.focus())}refreshSendState(){const e=!this.input||this.input.value.trim().length===0;this.sendBtn&&(this.sendBtn.disabled=e||this.streaming)}autosize(){const e=this.input;e&&(e.style.height="auto",e.style.height=Math.min(e.scrollHeight,120)+"px")}appendBubble(e,r){const t=document.createElement("div");return t.className=`msg ${e}`,t.textContent=r,this.list.appendChild(t),this.scrollToBottom(),t}scrollToBottom(){this.list&&(this.list.scrollTop=this.list.scrollHeight)}async send(){if(this.streaming||!this.input)return;const e=this.input.value.trim();if(!e)return;if(!this.serverUrl||!this.publicKey){this.appendBubble("assistant","⚠ Widget misconfigured: missing server-url or public-key.").classList.add("error");return}this.input.value="",this.autosize(),this.messages.push({role:"user",content:e}),this.appendBubble("user",e);const r=this.appendBubble("assistant","");r.classList.add("pending"),this.streaming=!0,this.refreshSendState(),this.controller=new AbortController;let t="";await w(this.serverUrl,{publicKey:this.publicKey,messages:this.messages,conversationId:this.conversationId},{onDelta:n=>{t+=n,r.textContent=t,this.scrollToBottom()},onDone:()=>{this.messages.push({role:"assistant",content:t})},onError:(n,c)=>{r.classList.add("error"),r.textContent=t?`${t}
|
|
211
|
+
|
|
212
|
+
⚠ ${c}`:`⚠ ${c} (${n})`,this.scrollToBottom()}},this.controller.signal),r.classList.remove("pending"),this.streaming=!1,this.controller=null,this.refreshSendState(),this.input.focus()}}function v(s){return s.replace(/[&<>]/g,a=>a==="&"?"&":a==="<"?"<":">")}function A(s){return v(s).replace(/"/g,""")}function C(){const s=globalThis.crypto;return s&&typeof s.randomUUID=="function"?s.randomUUID():"c-"+Math.random().toString(36).slice(2)+Date.now().toString(36)}typeof customElements<"u"&&!customElements.get(f)&&customElements.define(f,x);function T(){if(typeof document>"u")return;const s=document.currentScript;if(!s)return;const a=s.getAttribute("data-public-key"),e=s.getAttribute("data-server-url");if(!a||!e||document.querySelector(f))return;const r=document.createElement(f);r.setAttribute("public-key",a),r.setAttribute("server-url",e);const t=s.getAttribute("data-title"),n=s.getAttribute("data-accent");t&&r.setAttribute("title",t),n&&r.setAttribute("accent",n);const c=()=>{document.body.appendChild(r)};document.body?c():document.addEventListener("DOMContentLoaded",c,{once:!0})}return T(),i.AIChatWidget=x,i.CONTRACT_VERSION=S,i.TAG=f,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"}),i})({});
|
|
213
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.js","sources":["../../shared/dist/index.js","../src/styles.ts","../src/sse.ts","../src/widget.ts","../src/embed.ts","../src/index.ts"],"sourcesContent":["// src/index.ts\nvar CONTRACT_VERSION = \"1\";\nvar CHAT_ENDPOINT = \"/v1/chat\";\nvar PROVIDER_IDS = [\"anthropic\", \"openai\", \"gemini\", \"openrouter\"];\nexport {\n CHAT_ENDPOINT,\n CONTRACT_VERSION,\n PROVIDER_IDS\n};\n","// All widget CSS, scoped inside the shadow root.\n//\n// `:host { all: initial }` resets every inherited property at the boundary so the\n// host page's font/color/box-sizing can't leak in; CSS custom properties (our\n// `--accent`) are unaffected by `all` and pass through. Nothing here escapes the\n// shadow root, so the page's styles are untouched too.\n\nexport const STYLES = /* css */ `\n:host {\n all: initial;\n --accent: #2563eb;\n --accent-contrast: #ffffff;\n --bg: #ffffff;\n --fg: #1f2329;\n --muted: #6b7280;\n --bubble-assistant: #f1f3f5;\n --radius: 16px;\n --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,\n sans-serif;\n}\n\n* { box-sizing: border-box; }\n\n.launcher {\n position: fixed;\n bottom: 20px;\n right: 20px;\n width: 56px;\n height: 56px;\n border-radius: 50%;\n border: none;\n background: var(--accent);\n color: var(--accent-contrast);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);\n z-index: 2147483000;\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n.launcher:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.22); }\n.launcher:active { transform: translateY(0); }\n.launcher svg { width: 26px; height: 26px; }\n\n.panel {\n position: fixed;\n bottom: 88px;\n right: 20px;\n width: 370px;\n max-width: calc(100vw - 40px);\n height: 540px;\n max-height: calc(100vh - 120px);\n background: var(--bg);\n color: var(--fg);\n font-family: var(--font);\n font-size: 14px;\n line-height: 1.5;\n border-radius: var(--radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.24);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n z-index: 2147483000;\n opacity: 0;\n transform: translateY(12px) scale(0.98);\n pointer-events: none;\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.panel.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }\n\n.header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 14px 16px;\n background: var(--accent);\n color: var(--accent-contrast);\n}\n.header .title { font-weight: 600; font-size: 15px; flex: 1; }\n.header .close {\n border: none;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font-size: 20px;\n line-height: 1;\n padding: 2px 6px;\n border-radius: 8px;\n opacity: 0.85;\n}\n.header .close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.messages:empty::before {\n content: attr(data-empty);\n color: var(--muted);\n margin: auto;\n text-align: center;\n padding: 0 24px;\n}\n\n.msg {\n max-width: 82%;\n padding: 9px 13px;\n border-radius: 14px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: anywhere;\n}\n.msg.user {\n align-self: flex-end;\n background: var(--accent);\n color: var(--accent-contrast);\n border-bottom-right-radius: 4px;\n}\n.msg.assistant {\n align-self: flex-start;\n background: var(--bubble-assistant);\n color: var(--fg);\n border-bottom-left-radius: 4px;\n}\n.msg.error { background: #fde8e8; color: #b42318; }\n.msg.pending::after {\n content: '▋';\n margin-left: 1px;\n animation: blink 1s steps(2, start) infinite;\n}\n@keyframes blink { to { visibility: hidden; } }\n\n.composer {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 10px;\n border-top: 1px solid #eceef1;\n}\n.composer textarea {\n flex: 1;\n resize: none;\n border: 1px solid #d7dade;\n border-radius: 12px;\n padding: 9px 12px;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n max-height: 120px;\n outline: none;\n background: var(--bg);\n color: var(--fg);\n}\n.composer textarea:focus { border-color: var(--accent); }\n.composer .send {\n border: none;\n background: var(--accent);\n color: var(--accent-contrast);\n width: 40px;\n height: 40px;\n border-radius: 12px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n.composer .send:disabled { opacity: 0.45; cursor: default; }\n.composer .send svg { width: 20px; height: 20px; }\n\n@media (max-width: 480px) {\n .panel {\n right: 10px;\n left: 10px;\n bottom: 10px;\n width: auto;\n height: auto;\n top: 10px;\n max-height: none;\n }\n .launcher { bottom: 16px; right: 16px; }\n}\n`;\n","// SSE chat client (WS3).\n//\n// POSTs a ChatRequest to `{serverUrl}{CHAT_ENDPOINT}` and reads the server's\n// text/event-stream response via fetch + ReadableStream — no EventSource (which\n// can't POST). Each SSE event's `data:` payload is one JSON ChatStreamEvent.\n\nimport { CHAT_ENDPOINT } from '@aichat/shared';\nimport type { ChatRequest, ChatStreamEvent } from '@aichat/shared';\n\nexport interface StreamHandlers {\n /** Incremental assistant text. */\n onDelta(text: string): void;\n /** Stream finished cleanly. */\n onDone(event: Extract<ChatStreamEvent, { type: 'done' }>): void;\n /** Transport or server-reported error; terminal. */\n onError(code: string, message: string): void;\n}\n\n/**\n * Stream one completion. Resolves when the stream ends (success or error);\n * all outcomes are reported through `handlers`, never thrown.\n */\nexport async function streamChat(\n serverUrl: string,\n request: ChatRequest,\n handlers: StreamHandlers,\n signal?: AbortSignal,\n): Promise<void> {\n const url = serverUrl.replace(/\\/+$/, '') + CHAT_ENDPOINT;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n },\n body: JSON.stringify(request),\n signal,\n });\n } catch (err) {\n if (isAbort(err)) return;\n handlers.onError('network', err instanceof Error ? err.message : 'Request failed');\n return;\n }\n\n if (!res.ok || !res.body) {\n handlers.onError(\n String(res.status || 'no_body'),\n res.statusText || 'No response stream',\n );\n return;\n }\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n // Strip CRs so events split cleanly whether the server uses \\n or \\r\\n.\n buffer += decoder.decode(value, { stream: true }).replace(/\\r/g, '');\n\n let sep: number;\n while ((sep = buffer.indexOf('\\n\\n')) !== -1) {\n const rawEvent = buffer.slice(0, sep);\n buffer = buffer.slice(sep + 2);\n if (dispatch(rawEvent, handlers)) return; // terminal event seen\n }\n }\n // Flush a trailing event that lacked the terminating blank line.\n if (buffer.trim()) dispatch(buffer, handlers);\n } catch (err) {\n if (isAbort(err)) return;\n handlers.onError('stream', err instanceof Error ? err.message : 'Stream failed');\n }\n}\n\n/** Parse one SSE event block. Returns true if it was a terminal event. */\nfunction dispatch(rawEvent: string, handlers: StreamHandlers): boolean {\n const dataLines: string[] = [];\n for (const line of rawEvent.split('\\n')) {\n if (line.startsWith(':')) continue; // comment / keep-alive\n if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return false;\n\n const payload = dataLines.join('\\n');\n if (!payload || payload === '[DONE]') return false;\n\n let event: ChatStreamEvent;\n try {\n event = JSON.parse(payload) as ChatStreamEvent;\n } catch {\n return false; // ignore unparseable lines\n }\n\n switch (event.type) {\n case 'delta':\n handlers.onDelta(event.text);\n return false;\n case 'done':\n handlers.onDone(event);\n return true;\n case 'error':\n handlers.onError(event.code, event.message);\n return true;\n default:\n return false;\n }\n}\n\nfunction isAbort(err: unknown): boolean {\n return err instanceof Error && err.name === 'AbortError';\n}\n","// <ai-chat-widget> — the framework-agnostic chat web component (WS3).\n//\n// Renders a floating launcher + chat panel entirely inside a Shadow DOM so the\n// host page's CSS/JS and the widget's are mutually isolated. Configuration comes\n// from attributes: `public-key`, `server-url` (required) and `title`, `accent`.\n\nimport { CONTRACT_VERSION as WIRE_CONTRACT_VERSION } from '@aichat/shared';\nimport type { ChatMessage } from '@aichat/shared';\n\n/**\n * Wire-contract version this widget speaks. Re-assigned to a local const (rather\n * than re-exported) so the emitted .d.ts inlines the literal and the published\n * type surface stays self-contained — no shared-package import for consumers.\n */\nexport const CONTRACT_VERSION = WIRE_CONTRACT_VERSION;\nimport { STYLES } from './styles';\nimport { streamChat } from './sse';\n\nexport const TAG = 'ai-chat-widget';\n\nconst LAUNCHER_ICON = /* html */ `\n<svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5Z\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\nconst SEND_ICON = /* html */ `\n<svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M22 2 11 13M22 2l-7 20-4-9-9-4 20-7Z\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>`;\n\nexport class AIChatWidget extends HTMLElement {\n /** Canonical conversation history sent on every request (no server persistence in M1). */\n private messages: ChatMessage[] = [];\n private conversationId = generateId();\n private open = false;\n private streaming = false;\n private controller: AbortController | null = null;\n\n // Cached shadow-root nodes, set up once on first connect.\n private panel?: HTMLElement;\n private launcher?: HTMLButtonElement;\n private list?: HTMLElement;\n private input?: HTMLTextAreaElement;\n private sendBtn?: HTMLButtonElement;\n private mounted = false;\n\n connectedCallback(): void {\n if (!this.shadowRoot) this.attachShadow({ mode: 'open' });\n if (!this.mounted) this.render();\n }\n\n disconnectedCallback(): void {\n this.controller?.abort();\n }\n\n private get serverUrl(): string {\n return this.getAttribute('server-url') ?? '';\n }\n private get publicKey(): string {\n return this.getAttribute('public-key') ?? '';\n }\n\n private render(): void {\n const root = this.shadowRoot!;\n const accent = this.getAttribute('accent');\n if (accent) this.style.setProperty('--accent', accent); // custom prop passes the shadow boundary\n const title = this.getAttribute('title') || 'Chat';\n\n root.innerHTML = `\n <style>${STYLES}</style>\n <button class=\"launcher\" type=\"button\" part=\"launcher\" aria-label=\"Open chat\">\n ${LAUNCHER_ICON}\n </button>\n <section class=\"panel\" role=\"dialog\" aria-label=\"${escapeAttr(title)}\" aria-modal=\"false\">\n <header class=\"header\">\n <span class=\"title\">${escapeHtml(title)}</span>\n <button class=\"close\" type=\"button\" aria-label=\"Close chat\">×</button>\n </header>\n <div class=\"messages\" role=\"log\" aria-live=\"polite\"\n data-empty=\"Send a message to start the conversation.\"></div>\n <form class=\"composer\">\n <textarea rows=\"1\" placeholder=\"Type a message…\" aria-label=\"Message\"></textarea>\n <button class=\"send\" type=\"submit\" aria-label=\"Send message\" disabled>${SEND_ICON}</button>\n </form>\n </section>\n `;\n\n this.launcher = root.querySelector('.launcher')!;\n this.panel = root.querySelector('.panel')!;\n this.list = root.querySelector('.messages')!;\n this.input = root.querySelector('textarea')!;\n this.sendBtn = root.querySelector('.send')!;\n const form = root.querySelector('.composer') as HTMLFormElement;\n const close = root.querySelector('.close') as HTMLButtonElement;\n\n this.launcher.addEventListener('click', () => this.toggle(true));\n close.addEventListener('click', () => this.toggle(false));\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n void this.send();\n });\n\n this.input.addEventListener('input', () => {\n this.autosize();\n this.refreshSendState();\n });\n this.input.addEventListener('keydown', (e) => {\n // Enter sends; Shift+Enter inserts a newline.\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n void this.send();\n }\n });\n\n this.mounted = true;\n }\n\n private toggle(open: boolean): void {\n this.open = open;\n this.panel?.classList.toggle('open', open);\n this.launcher?.setAttribute('aria-label', open ? 'Close chat' : 'Open chat');\n if (open) this.input?.focus();\n }\n\n private refreshSendState(): void {\n const empty = !this.input || this.input.value.trim().length === 0;\n if (this.sendBtn) this.sendBtn.disabled = empty || this.streaming;\n }\n\n private autosize(): void {\n const el = this.input;\n if (!el) return;\n el.style.height = 'auto';\n el.style.height = Math.min(el.scrollHeight, 120) + 'px';\n }\n\n private appendBubble(role: ChatMessage['role'], text: string): HTMLElement {\n const bubble = document.createElement('div');\n bubble.className = `msg ${role}`;\n bubble.textContent = text;\n this.list!.appendChild(bubble);\n this.scrollToBottom();\n return bubble;\n }\n\n private scrollToBottom(): void {\n if (this.list) this.list.scrollTop = this.list.scrollHeight;\n }\n\n private async send(): Promise<void> {\n if (this.streaming || !this.input) return;\n const text = this.input.value.trim();\n if (!text) return;\n\n if (!this.serverUrl || !this.publicKey) {\n this.appendBubble('assistant', '⚠ Widget misconfigured: missing server-url or public-key.')\n .classList.add('error');\n return;\n }\n\n // User turn.\n this.input.value = '';\n this.autosize();\n this.messages.push({ role: 'user', content: text });\n this.appendBubble('user', text);\n\n // Assistant turn — stream into a live bubble.\n const bubble = this.appendBubble('assistant', '');\n bubble.classList.add('pending');\n this.streaming = true;\n this.refreshSendState();\n this.controller = new AbortController();\n\n let acc = '';\n await streamChat(\n this.serverUrl,\n { publicKey: this.publicKey, messages: this.messages, conversationId: this.conversationId },\n {\n onDelta: (delta) => {\n acc += delta;\n bubble.textContent = acc;\n this.scrollToBottom();\n },\n onDone: () => {\n this.messages.push({ role: 'assistant', content: acc });\n },\n onError: (code, message) => {\n bubble.classList.add('error');\n bubble.textContent = acc ? `${acc}\\n\\n⚠ ${message}` : `⚠ ${message} (${code})`;\n this.scrollToBottom();\n },\n },\n this.controller.signal,\n );\n\n bubble.classList.remove('pending');\n this.streaming = false;\n this.controller = null;\n this.refreshSendState();\n this.input.focus();\n }\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>]/g, (c) => (c === '&' ? '&' : c === '<' ? '<' : '>'));\n}\nfunction escapeAttr(s: string): string {\n return escapeHtml(s).replace(/\"/g, '"');\n}\n\nfunction generateId(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === 'function') return c.randomUUID();\n return 'c-' + Math.random().toString(36).slice(2) + Date.now().toString(36);\n}\n\n// Register the element on import. Both the ESM and embed builds pull this in.\nif (typeof customElements !== 'undefined' && !customElements.get(TAG)) {\n customElements.define(TAG, AIChatWidget);\n}\n\n","// Auto-inject path for the plain <script> embed (WS3).\n//\n// When the bundle is loaded as `embed.js` via a <script> tag, `document.currentScript`\n// is that tag — we read its data-* attributes and drop an <ai-chat-widget> into the\n// page. When the same bundle is imported as an ES module (framework apps),\n// `document.currentScript` is null, so this is a no-op and the host mounts the\n// element itself.\n\nimport { TAG } from './widget';\n\nexport function autoInject(): void {\n if (typeof document === 'undefined') return;\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return; // ESM import — nothing to auto-mount from.\n\n const publicKey = script.getAttribute('data-public-key');\n const serverUrl = script.getAttribute('data-server-url');\n if (!publicKey || !serverUrl) return; // not configured for auto-embed\n if (document.querySelector(TAG)) return; // already present\n\n const el = document.createElement(TAG);\n el.setAttribute('public-key', publicKey);\n el.setAttribute('server-url', serverUrl);\n const title = script.getAttribute('data-title');\n const accent = script.getAttribute('data-accent');\n if (title) el.setAttribute('title', title);\n if (accent) el.setAttribute('accent', accent);\n\n const mount = (): void => {\n document.body.appendChild(el);\n };\n if (document.body) mount();\n else document.addEventListener('DOMContentLoaded', mount, { once: true });\n}\n","// @dgrtechlabs/chat-widget — framework-agnostic embeddable chat widget (WS3 entry).\n//\n// Single entry built two ways (see vite.config.ts):\n// • ESM `aichat-widget.js` — `import '@dgrtechlabs/chat-widget'` registers <ai-chat-widget>;\n// the host app places the element. autoInject() is a no-op (no currentScript).\n// • IIFE `embed.js` — drop-in <script> tag; autoInject() reads the tag's\n// data-public-key / data-server-url and mounts the widget into <body>.\n\nimport { AIChatWidget, TAG, CONTRACT_VERSION } from './widget';\nimport { autoInject } from './embed';\n\n// Runs synchronously at load so `document.currentScript` is still the embed tag.\nautoInject();\n\nexport { AIChatWidget, TAG, CONTRACT_VERSION };\n"],"names":["CONTRACT_VERSION","CHAT_ENDPOINT","STYLES","streamChat","serverUrl","request","handlers","signal","url","res","err","isAbort","reader","decoder","buffer","value","done","sep","rawEvent","dispatch","dataLines","line","payload","event","WIRE_CONTRACT_VERSION","TAG","LAUNCHER_ICON","SEND_ICON","AIChatWidget","__publicField","generateId","_a","root","accent","title","escapeAttr","escapeHtml","form","close","e","open","_b","_c","empty","el","role","text","bubble","acc","delta","code","message","c","autoInject","script","publicKey","mount"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChatRequest, ChatStreamEvent } from '@aichat/shared';
|
|
2
|
+
export interface StreamHandlers {
|
|
3
|
+
/** Incremental assistant text. */
|
|
4
|
+
onDelta(text: string): void;
|
|
5
|
+
/** Stream finished cleanly. */
|
|
6
|
+
onDone(event: Extract<ChatStreamEvent, {
|
|
7
|
+
type: 'done';
|
|
8
|
+
}>): void;
|
|
9
|
+
/** Transport or server-reported error; terminal. */
|
|
10
|
+
onError(code: string, message: string): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Stream one completion. Resolves when the stream ends (success or error);
|
|
14
|
+
* all outcomes are reported through `handlers`, never thrown.
|
|
15
|
+
*/
|
|
16
|
+
export declare function streamChat(serverUrl: string, request: ChatRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STYLES = "\n:host {\n all: initial;\n --accent: #2563eb;\n --accent-contrast: #ffffff;\n --bg: #ffffff;\n --fg: #1f2329;\n --muted: #6b7280;\n --bubble-assistant: #f1f3f5;\n --radius: 16px;\n --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,\n sans-serif;\n}\n\n* { box-sizing: border-box; }\n\n.launcher {\n position: fixed;\n bottom: 20px;\n right: 20px;\n width: 56px;\n height: 56px;\n border-radius: 50%;\n border: none;\n background: var(--accent);\n color: var(--accent-contrast);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);\n z-index: 2147483000;\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n.launcher:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.22); }\n.launcher:active { transform: translateY(0); }\n.launcher svg { width: 26px; height: 26px; }\n\n.panel {\n position: fixed;\n bottom: 88px;\n right: 20px;\n width: 370px;\n max-width: calc(100vw - 40px);\n height: 540px;\n max-height: calc(100vh - 120px);\n background: var(--bg);\n color: var(--fg);\n font-family: var(--font);\n font-size: 14px;\n line-height: 1.5;\n border-radius: var(--radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.24);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n z-index: 2147483000;\n opacity: 0;\n transform: translateY(12px) scale(0.98);\n pointer-events: none;\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.panel.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }\n\n.header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 14px 16px;\n background: var(--accent);\n color: var(--accent-contrast);\n}\n.header .title { font-weight: 600; font-size: 15px; flex: 1; }\n.header .close {\n border: none;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font-size: 20px;\n line-height: 1;\n padding: 2px 6px;\n border-radius: 8px;\n opacity: 0.85;\n}\n.header .close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.messages:empty::before {\n content: attr(data-empty);\n color: var(--muted);\n margin: auto;\n text-align: center;\n padding: 0 24px;\n}\n\n.msg {\n max-width: 82%;\n padding: 9px 13px;\n border-radius: 14px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: anywhere;\n}\n.msg.user {\n align-self: flex-end;\n background: var(--accent);\n color: var(--accent-contrast);\n border-bottom-right-radius: 4px;\n}\n.msg.assistant {\n align-self: flex-start;\n background: var(--bubble-assistant);\n color: var(--fg);\n border-bottom-left-radius: 4px;\n}\n.msg.error { background: #fde8e8; color: #b42318; }\n.msg.pending::after {\n content: '\u258B';\n margin-left: 1px;\n animation: blink 1s steps(2, start) infinite;\n}\n@keyframes blink { to { visibility: hidden; } }\n\n.composer {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 10px;\n border-top: 1px solid #eceef1;\n}\n.composer textarea {\n flex: 1;\n resize: none;\n border: 1px solid #d7dade;\n border-radius: 12px;\n padding: 9px 12px;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n max-height: 120px;\n outline: none;\n background: var(--bg);\n color: var(--fg);\n}\n.composer textarea:focus { border-color: var(--accent); }\n.composer .send {\n border: none;\n background: var(--accent);\n color: var(--accent-contrast);\n width: 40px;\n height: 40px;\n border-radius: 12px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n.composer .send:disabled { opacity: 0.45; cursor: default; }\n.composer .send svg { width: 20px; height: 20px; }\n\n@media (max-width: 480px) {\n .panel {\n right: 10px;\n left: 10px;\n bottom: 10px;\n width: auto;\n height: auto;\n top: 10px;\n max-height: none;\n }\n .launcher { bottom: 16px; right: 16px; }\n}\n";
|
package/dist/widget.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-contract version this widget speaks. Re-assigned to a local const (rather
|
|
3
|
+
* than re-exported) so the emitted .d.ts inlines the literal and the published
|
|
4
|
+
* type surface stays self-contained — no shared-package import for consumers.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONTRACT_VERSION: "1";
|
|
7
|
+
export declare const TAG = "ai-chat-widget";
|
|
8
|
+
export declare class AIChatWidget extends HTMLElement {
|
|
9
|
+
/** Canonical conversation history sent on every request (no server persistence in M1). */
|
|
10
|
+
private messages;
|
|
11
|
+
private conversationId;
|
|
12
|
+
private open;
|
|
13
|
+
private streaming;
|
|
14
|
+
private controller;
|
|
15
|
+
private panel?;
|
|
16
|
+
private launcher?;
|
|
17
|
+
private list?;
|
|
18
|
+
private input?;
|
|
19
|
+
private sendBtn?;
|
|
20
|
+
private mounted;
|
|
21
|
+
connectedCallback(): void;
|
|
22
|
+
disconnectedCallback(): void;
|
|
23
|
+
private get serverUrl();
|
|
24
|
+
private get publicKey();
|
|
25
|
+
private render;
|
|
26
|
+
private toggle;
|
|
27
|
+
private refreshSendState;
|
|
28
|
+
private autosize;
|
|
29
|
+
private appendBubble;
|
|
30
|
+
private scrollToBottom;
|
|
31
|
+
private send;
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dgrtechlabs/chat-widget",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Embeddable, framework-agnostic AI chat widget — a Shadow-DOM web component with a one-line <script> embed and streaming (SSE) replies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/aichat-widget.js",
|
|
8
|
+
"module": "./dist/aichat-widget.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/aichat-widget.js",
|
|
14
|
+
"default": "./dist/aichat-widget.js"
|
|
15
|
+
},
|
|
16
|
+
"./embed": "./dist/embed.js",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"unpkg": "./dist/embed.js",
|
|
20
|
+
"jsdelivr": "./dist/embed.js",
|
|
21
|
+
"sideEffects": true,
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"chat",
|
|
27
|
+
"chat-widget",
|
|
28
|
+
"ai",
|
|
29
|
+
"chatbot",
|
|
30
|
+
"web-component",
|
|
31
|
+
"custom-element",
|
|
32
|
+
"embeddable",
|
|
33
|
+
"shadow-dom",
|
|
34
|
+
"sse",
|
|
35
|
+
"streaming"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/ndogar87/aichatbot.git",
|
|
40
|
+
"directory": "packages/widget"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/ndogar87/aichatbot/tree/main/packages/widget#readme",
|
|
43
|
+
"bugs": "https://github.com/ndogar87/aichatbot/issues",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.6.3",
|
|
49
|
+
"vite": "^6.0.3",
|
|
50
|
+
"@aichat/shared": "0.0.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
54
|
+
"dev": "vite",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|