@trossino/chat-widget 0.1.0 → 0.2.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 +74 -11
- package/dist/index.cjs +182 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +188 -128
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ export const POST = createChatHandler({
|
|
|
61
61
|
import { ChatWidget } from '@trossino/chat-widget';
|
|
62
62
|
|
|
63
63
|
export default function Page() {
|
|
64
|
-
return <ChatWidget endpoint="/api/chat"
|
|
64
|
+
return <ChatWidget endpoint="/api/chat" />;
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
@@ -71,15 +71,78 @@ The handler is a plain `(req: Request) => Promise<Response>`, so it also works a
|
|
|
71
71
|
|
|
72
72
|
## `<ChatWidget>` props
|
|
73
73
|
|
|
74
|
-
| Prop | Type | Default
|
|
75
|
-
| ---------------- | ------------------------ |
|
|
76
|
-
| `endpoint` | `string` | —
|
|
77
|
-
| `headers` | `Record<string, string>` | —
|
|
78
|
-
| `placeholder` | `string` | `"
|
|
79
|
-
| `title` | `string` | `"Chat"`
|
|
80
|
-
| `welcomeMessage` | `string` | —
|
|
81
|
-
| `onError` | `(error: Error) => void` | —
|
|
82
|
-
| `defaultOpen` | `boolean` | `false`
|
|
74
|
+
| Prop | Type | Default | Description |
|
|
75
|
+
| ---------------- | ------------------------ | -------------------- | ------------------------------------------------------------------------ |
|
|
76
|
+
| `endpoint` | `string` | — | URL receiving `POST { messages }`. Required. |
|
|
77
|
+
| `headers` | `Record<string, string>` | — | Extra request headers (e.g. a session token). **Never a provider key.** |
|
|
78
|
+
| `placeholder` | `string` | `"Ask me anything…"` | Input placeholder. |
|
|
79
|
+
| `title` | `string` | `"Chat with me"` | Panel title. |
|
|
80
|
+
| `welcomeMessage` | `string` | — | Shown when the conversation is empty. Display-only, not sent to the API. |
|
|
81
|
+
| `onError` | `(error: Error) => void` | — | Called on network, HTTP, or stream errors. |
|
|
82
|
+
| `defaultOpen` | `boolean` | `false` | Open the panel on mount. |
|
|
83
|
+
| `variant` | `'light' \| 'dark'` | `'light'` | Built-in palette. See [Theming](#theming). |
|
|
84
|
+
| `className` | `string` | — | Extra class on the root element (it always has `tr-chat-widget`). |
|
|
85
|
+
| `style` | `ChatWidgetStyle` | — | Inline styles merged onto the root. Accepts `--cw-*` properties. |
|
|
86
|
+
| `toggleIcon` | `ReactNode` | chat bubble icon | Custom launcher icon, shown while the panel is closed. |
|
|
87
|
+
|
|
88
|
+
## Theming
|
|
89
|
+
|
|
90
|
+
Every color, radius, shadow, and font is read from a CSS custom property on the root element, with a built-in fallback. Set the properties from a class (via `className`) or inline (via `style`) and the widget picks them up. Anything you set wins over `variant`; anything you don't set falls back to the variant's value.
|
|
91
|
+
|
|
92
|
+
| Property | Light default | Dark default | Used for |
|
|
93
|
+
| ----------------------- | ------------------------------------------------------------------- | ------------------------ | ------------------------------------------- |
|
|
94
|
+
| `--cw-font` | `system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif` | same | Font family |
|
|
95
|
+
| `--cw-text` | `#111827` | `#e5e0db` | Base text |
|
|
96
|
+
| `--cw-muted` | `#6b7280` | `#8a8580` | "Typing…" indicator |
|
|
97
|
+
| `--cw-panel-bg` | `#ffffff` | `#0c0c0c` | Panel and input bar background |
|
|
98
|
+
| `--cw-list-bg` | `#f9fafb` | `#0a0a0a` | Message list background |
|
|
99
|
+
| `--cw-border` | `#e5e7eb` | `rgba(255,255,255,0.08)` | Panel, assistant bubble, input bar borders |
|
|
100
|
+
| `--cw-accent` | `#2563eb` | same | Launcher, header, user bubble, send, focus |
|
|
101
|
+
| `--cw-accent-contrast` | `#ffffff` | same | Text/icons on accent |
|
|
102
|
+
| `--cw-bubble-bg` | `#ffffff` | `rgba(255,255,255,0.04)` | Assistant bubble background |
|
|
103
|
+
| `--cw-bubble-text` | `#111827` | `#e5e0db` | Assistant bubble text |
|
|
104
|
+
| `--cw-input-bg` | `#ffffff` | `rgba(255,255,255,0.03)` | Textarea background |
|
|
105
|
+
| `--cw-input-border` | `#d1d5db` | `rgba(255,255,255,0.08)` | Textarea border |
|
|
106
|
+
| `--cw-error-bg` | `#fef2f2` | `rgba(185,28,28,0.15)` | Error banner background |
|
|
107
|
+
| `--cw-error-text` | `#b91c1c` | `#fca5a5` | Error banner text |
|
|
108
|
+
| `--cw-shadow` | `0 12px 40px rgba(0,0,0,0.18)` | same | Panel shadow |
|
|
109
|
+
| `--cw-toggle-shadow` | `0 6px 20px rgba(0,0,0,0.2)` | same | Launcher shadow |
|
|
110
|
+
| `--cw-radius` | `12px` | same | Panel and bubbles |
|
|
111
|
+
| `--cw-radius-sm` | `8px` | same | Input, send button, error banner |
|
|
112
|
+
| `--cw-header-bg` | `var(--cw-accent)` | same | Header background |
|
|
113
|
+
| `--cw-header-text` | `var(--cw-accent-contrast)` | same | Header title and close button |
|
|
114
|
+
| `--cw-header-border` | `none` | same | Header `border-bottom` shorthand |
|
|
115
|
+
| `--cw-title-font` | `inherit` | same | Header title font family |
|
|
116
|
+
|
|
117
|
+
Quick accent change, inline:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<ChatWidget endpoint="/api/chat" style={{ '--cw-accent': '#c8553d' }} />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Dark theme example
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<ChatWidget endpoint="/api/chat" variant="dark" className="site-chat" />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
/* Tune on top of the dark variant: only what differs needs setting. */
|
|
131
|
+
.site-chat {
|
|
132
|
+
--cw-accent: #c8553d;
|
|
133
|
+
--cw-font: "DM Sans", ui-sans-serif, system-ui, sans-serif;
|
|
134
|
+
--cw-radius: 10px;
|
|
135
|
+
--cw-radius-sm: 6px;
|
|
136
|
+
--cw-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
|
137
|
+
|
|
138
|
+
/* Editorial header: no accent fill, just a hairline. */
|
|
139
|
+
--cw-header-bg: transparent;
|
|
140
|
+
--cw-header-text: #e5e0db;
|
|
141
|
+
--cw-header-border: 1px solid rgba(255, 255, 255, 0.08);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For deeper changes, every element also has a stable class (`tr-chat-panel`, `tr-chat-header`, `tr-chat-msg-user`, `tr-chat-input`, …) you can target with your own CSS, using `!important` where it needs to beat the inline styles.
|
|
83
146
|
|
|
84
147
|
## `createChatHandler` options
|
|
85
148
|
|
|
@@ -99,7 +162,7 @@ Responses: `200` with `{ reply }` or an SSE stream, `400` for an invalid body, `
|
|
|
99
162
|
|
|
100
163
|
## Optional styles
|
|
101
164
|
|
|
102
|
-
Everything is inline-styled. For extras inline styles can't express (focus rings
|
|
165
|
+
Everything is inline-styled. For extras inline styles can't express (focus rings in `--cw-accent`, hover, entry animation that respects `prefers-reduced-motion`):
|
|
103
166
|
|
|
104
167
|
```ts
|
|
105
168
|
import '@trossino/chat-widget/styles';
|
package/dist/index.cjs
CHANGED
|
@@ -166,139 +166,194 @@ async function readSSE(body, onDelta) {
|
|
|
166
166
|
|
|
167
167
|
// src/client/ChatWidget.tsx
|
|
168
168
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
169
|
-
var ACCENT = "#2563eb";
|
|
170
169
|
var FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
171
|
-
var
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
170
|
+
var LIGHT = {
|
|
171
|
+
font: FONT,
|
|
172
|
+
text: "#111827",
|
|
173
|
+
muted: "#6b7280",
|
|
174
|
+
"panel-bg": "#ffffff",
|
|
175
|
+
"list-bg": "#f9fafb",
|
|
176
|
+
border: "#e5e7eb",
|
|
177
|
+
accent: "#2563eb",
|
|
178
|
+
"accent-contrast": "#ffffff",
|
|
179
|
+
"bubble-bg": "#ffffff",
|
|
180
|
+
"bubble-text": "#111827",
|
|
181
|
+
"input-bg": "#ffffff",
|
|
182
|
+
"input-border": "#d1d5db",
|
|
183
|
+
"error-bg": "#fef2f2",
|
|
184
|
+
"error-text": "#b91c1c",
|
|
185
|
+
shadow: "0 12px 40px rgba(0,0,0,0.18)",
|
|
186
|
+
"toggle-shadow": "0 6px 20px rgba(0,0,0,0.2)",
|
|
187
|
+
radius: "12px",
|
|
188
|
+
"radius-sm": "8px"
|
|
189
|
+
};
|
|
190
|
+
var DARK = {
|
|
191
|
+
...LIGHT,
|
|
192
|
+
text: "#e5e0db",
|
|
193
|
+
muted: "#8a8580",
|
|
194
|
+
"panel-bg": "#0c0c0c",
|
|
195
|
+
"list-bg": "#0a0a0a",
|
|
196
|
+
border: "rgba(255,255,255,0.08)",
|
|
197
|
+
"bubble-bg": "rgba(255,255,255,0.04)",
|
|
198
|
+
"bubble-text": "#e5e0db",
|
|
199
|
+
"input-bg": "rgba(255,255,255,0.03)",
|
|
200
|
+
"input-border": "rgba(255,255,255,0.08)",
|
|
201
|
+
"error-bg": "rgba(185,28,28,0.15)",
|
|
202
|
+
"error-text": "#fca5a5"
|
|
203
|
+
};
|
|
204
|
+
function createStyles(theme) {
|
|
205
|
+
const v = (name) => `var(--cw-${name}, ${theme[name]})`;
|
|
206
|
+
const accent = v("accent");
|
|
207
|
+
const accentContrast = v("accent-contrast");
|
|
208
|
+
const border = `1px solid ${v("border")}`;
|
|
209
|
+
return {
|
|
210
|
+
root: {
|
|
211
|
+
position: "fixed",
|
|
212
|
+
right: 20,
|
|
213
|
+
bottom: 20,
|
|
214
|
+
zIndex: 2147483e3,
|
|
215
|
+
fontFamily: v("font"),
|
|
216
|
+
fontSize: 14,
|
|
217
|
+
lineHeight: 1.45,
|
|
218
|
+
color: v("text"),
|
|
219
|
+
boxSizing: "border-box"
|
|
220
|
+
},
|
|
221
|
+
toggle: {
|
|
222
|
+
width: 56,
|
|
223
|
+
height: 56,
|
|
224
|
+
borderRadius: "50%",
|
|
225
|
+
border: "none",
|
|
226
|
+
background: accent,
|
|
227
|
+
color: accentContrast,
|
|
228
|
+
cursor: "pointer",
|
|
229
|
+
boxShadow: v("toggle-shadow"),
|
|
230
|
+
display: "flex",
|
|
231
|
+
alignItems: "center",
|
|
232
|
+
justifyContent: "center",
|
|
233
|
+
marginLeft: "auto",
|
|
234
|
+
padding: 0
|
|
235
|
+
},
|
|
236
|
+
panel: {
|
|
237
|
+
width: "min(370px, calc(100vw - 40px))",
|
|
238
|
+
height: "min(540px, calc(100vh - 110px))",
|
|
239
|
+
marginBottom: 12,
|
|
240
|
+
background: v("panel-bg"),
|
|
241
|
+
borderRadius: v("radius"),
|
|
242
|
+
boxShadow: v("shadow"),
|
|
243
|
+
border,
|
|
244
|
+
display: "flex",
|
|
245
|
+
flexDirection: "column",
|
|
246
|
+
overflow: "hidden"
|
|
247
|
+
},
|
|
248
|
+
header: {
|
|
249
|
+
display: "flex",
|
|
250
|
+
alignItems: "center",
|
|
251
|
+
justifyContent: "space-between",
|
|
252
|
+
padding: "12px 14px",
|
|
253
|
+
background: `var(--cw-header-bg, ${accent})`,
|
|
254
|
+
color: `var(--cw-header-text, ${accentContrast})`,
|
|
255
|
+
borderBottom: "var(--cw-header-border, none)"
|
|
256
|
+
},
|
|
257
|
+
title: { margin: 0, fontSize: 15, fontWeight: 600, fontFamily: "var(--cw-title-font, inherit)" },
|
|
258
|
+
close: {
|
|
259
|
+
background: "transparent",
|
|
260
|
+
border: "none",
|
|
261
|
+
color: "inherit",
|
|
262
|
+
cursor: "pointer",
|
|
263
|
+
fontSize: 20,
|
|
264
|
+
lineHeight: 1,
|
|
265
|
+
padding: 4
|
|
266
|
+
},
|
|
267
|
+
list: {
|
|
268
|
+
flex: 1,
|
|
269
|
+
overflowY: "auto",
|
|
270
|
+
padding: 14,
|
|
271
|
+
display: "flex",
|
|
272
|
+
flexDirection: "column",
|
|
273
|
+
gap: 8,
|
|
274
|
+
background: v("list-bg"),
|
|
275
|
+
margin: 0,
|
|
276
|
+
listStyle: "none"
|
|
277
|
+
},
|
|
278
|
+
bubble: {
|
|
279
|
+
maxWidth: "85%",
|
|
280
|
+
padding: "8px 12px",
|
|
281
|
+
borderRadius: v("radius"),
|
|
282
|
+
whiteSpace: "pre-wrap",
|
|
283
|
+
wordBreak: "break-word"
|
|
284
|
+
},
|
|
285
|
+
user: { alignSelf: "flex-end", background: accent, color: accentContrast, borderBottomRightRadius: 4 },
|
|
286
|
+
assistant: {
|
|
287
|
+
alignSelf: "flex-start",
|
|
288
|
+
background: v("bubble-bg"),
|
|
289
|
+
color: v("bubble-text"),
|
|
290
|
+
border,
|
|
291
|
+
borderBottomLeftRadius: 4
|
|
292
|
+
},
|
|
293
|
+
status: { alignSelf: "flex-start", color: v("muted"), fontStyle: "italic", padding: "0 4px" },
|
|
294
|
+
error: {
|
|
295
|
+
margin: "0 14px 8px",
|
|
296
|
+
padding: "8px 10px",
|
|
297
|
+
borderRadius: v("radius-sm"),
|
|
298
|
+
background: v("error-bg"),
|
|
299
|
+
color: v("error-text"),
|
|
300
|
+
fontSize: 13
|
|
301
|
+
},
|
|
302
|
+
form: { display: "flex", gap: 8, padding: 10, borderTop: border, background: v("panel-bg") },
|
|
303
|
+
input: {
|
|
304
|
+
flex: 1,
|
|
305
|
+
resize: "none",
|
|
306
|
+
border: `1px solid ${v("input-border")}`,
|
|
307
|
+
borderRadius: v("radius-sm"),
|
|
308
|
+
padding: "8px 10px",
|
|
309
|
+
font: "inherit",
|
|
310
|
+
color: "inherit",
|
|
311
|
+
background: v("input-bg"),
|
|
312
|
+
minHeight: 38,
|
|
313
|
+
maxHeight: 120,
|
|
314
|
+
boxSizing: "border-box"
|
|
315
|
+
},
|
|
316
|
+
send: {
|
|
317
|
+
border: "none",
|
|
318
|
+
borderRadius: v("radius-sm"),
|
|
319
|
+
background: accent,
|
|
320
|
+
color: accentContrast,
|
|
321
|
+
padding: "0 14px",
|
|
322
|
+
font: "inherit",
|
|
323
|
+
fontWeight: 600,
|
|
324
|
+
cursor: "pointer"
|
|
325
|
+
},
|
|
326
|
+
srOnly: {
|
|
327
|
+
position: "absolute",
|
|
328
|
+
width: 1,
|
|
329
|
+
height: 1,
|
|
330
|
+
padding: 0,
|
|
331
|
+
margin: -1,
|
|
332
|
+
overflow: "hidden",
|
|
333
|
+
clip: "rect(0,0,0,0)",
|
|
334
|
+
whiteSpace: "nowrap",
|
|
335
|
+
border: 0
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var STYLES = {
|
|
340
|
+
light: createStyles(LIGHT),
|
|
341
|
+
dark: createStyles(DARK)
|
|
292
342
|
};
|
|
293
343
|
function ChatWidget({
|
|
294
344
|
endpoint,
|
|
295
345
|
headers,
|
|
296
|
-
placeholder = "
|
|
297
|
-
title = "Chat",
|
|
346
|
+
placeholder = "Ask me anything\u2026",
|
|
347
|
+
title = "Chat with me",
|
|
298
348
|
welcomeMessage,
|
|
299
349
|
onError,
|
|
300
|
-
defaultOpen = false
|
|
350
|
+
defaultOpen = false,
|
|
351
|
+
variant = "light",
|
|
352
|
+
className,
|
|
353
|
+
style,
|
|
354
|
+
toggleIcon
|
|
301
355
|
}) {
|
|
356
|
+
const styles = STYLES[variant] ?? STYLES.light;
|
|
302
357
|
const [open, setOpen] = (0, import_react2.useState)(defaultOpen);
|
|
303
358
|
const [input, setInput] = (0, import_react2.useState)("");
|
|
304
359
|
const { messages, loading, error, send } = useChat({ endpoint, headers, onError });
|
|
@@ -342,7 +397,7 @@ function ChatWidget({
|
|
|
342
397
|
const last = messages[messages.length - 1];
|
|
343
398
|
const waitingFirstToken = loading && last?.role !== "assistant";
|
|
344
399
|
const canSend = input.trim().length > 0 && !loading;
|
|
345
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.root, className: "tr-chat-widget", children: [
|
|
400
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...styles.root, ...style }, className: className ? `tr-chat-widget ${className}` : "tr-chat-widget", children: [
|
|
346
401
|
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
347
402
|
"div",
|
|
348
403
|
{
|
|
@@ -416,7 +471,7 @@ function ChatWidget({
|
|
|
416
471
|
"aria-controls": open ? panelId : void 0,
|
|
417
472
|
style: styles.toggle,
|
|
418
473
|
className: "tr-chat-toggle",
|
|
419
|
-
children: open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M6 6l12 12M18 6L6 18" }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z" }) })
|
|
474
|
+
children: open ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M6 6l12 12M18 6L6 18" }) }) : toggleIcon !== void 0 ? toggleIcon : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z" }) })
|
|
420
475
|
}
|
|
421
476
|
)
|
|
422
477
|
] });
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client/ChatWidget.tsx","../src/client/useChat.ts"],"sourcesContent":["export * from './client';\nexport type { ChatMessage, ChatRole, ChatRequestBody, ChatResponseBody, ChatStreamChunk } from './types';\n","import { useEffect, useId, useRef, useState, type CSSProperties, type FormEvent, type KeyboardEvent } from 'react';\nimport { useChat } from './useChat';\n\nexport interface ChatWidgetProps {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request. Never put provider API keys here: they would be public. */\n headers?: Record<string, string>;\n /** Input placeholder. */\n placeholder?: string;\n /** Panel title. */\n title?: string;\n /** Message shown when the conversation is empty. Display-only: not sent to the backend. */\n welcomeMessage?: string;\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n /** Open the panel on mount. */\n defaultOpen?: boolean;\n}\n\nconst ACCENT = '#2563eb';\nconst FONT = 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\nconst styles = {\n root: {\n position: 'fixed',\n right: 20,\n bottom: 20,\n zIndex: 2147483000,\n fontFamily: FONT,\n fontSize: 14,\n lineHeight: 1.45,\n color: '#111827',\n boxSizing: 'border-box',\n },\n toggle: {\n width: 56,\n height: 56,\n borderRadius: '50%',\n border: 'none',\n background: ACCENT,\n color: '#fff',\n cursor: 'pointer',\n boxShadow: '0 6px 20px rgba(0,0,0,0.2)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n marginLeft: 'auto',\n padding: 0,\n },\n panel: {\n width: 'min(370px, calc(100vw - 40px))',\n height: 'min(540px, calc(100vh - 110px))',\n marginBottom: 12,\n background: '#fff',\n borderRadius: 12,\n boxShadow: '0 12px 40px rgba(0,0,0,0.18)',\n border: '1px solid #e5e7eb',\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n background: ACCENT,\n color: '#fff',\n },\n title: { margin: 0, fontSize: 15, fontWeight: 600 },\n close: {\n background: 'transparent',\n border: 'none',\n color: '#fff',\n cursor: 'pointer',\n fontSize: 20,\n lineHeight: 1,\n padding: 4,\n },\n list: {\n flex: 1,\n overflowY: 'auto',\n padding: 14,\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n background: '#f9fafb',\n margin: 0,\n listStyle: 'none',\n },\n bubble: {\n maxWidth: '85%',\n padding: '8px 12px',\n borderRadius: 12,\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n },\n user: { alignSelf: 'flex-end', background: ACCENT, color: '#fff', borderBottomRightRadius: 4 },\n assistant: { alignSelf: 'flex-start', background: '#fff', border: '1px solid #e5e7eb', borderBottomLeftRadius: 4 },\n status: { alignSelf: 'flex-start', color: '#6b7280', fontStyle: 'italic', padding: '0 4px' },\n error: {\n margin: '0 14px 8px',\n padding: '8px 10px',\n borderRadius: 8,\n background: '#fef2f2',\n color: '#b91c1c',\n fontSize: 13,\n },\n form: { display: 'flex', gap: 8, padding: 10, borderTop: '1px solid #e5e7eb', background: '#fff' },\n input: {\n flex: 1,\n resize: 'none',\n border: '1px solid #d1d5db',\n borderRadius: 8,\n padding: '8px 10px',\n font: 'inherit',\n color: 'inherit',\n background: '#fff',\n minHeight: 38,\n maxHeight: 120,\n boxSizing: 'border-box',\n },\n send: {\n border: 'none',\n borderRadius: 8,\n background: ACCENT,\n color: '#fff',\n padding: '0 14px',\n font: 'inherit',\n fontWeight: 600,\n cursor: 'pointer',\n },\n srOnly: {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n whiteSpace: 'nowrap',\n border: 0,\n },\n} satisfies Record<string, CSSProperties>;\n\nexport function ChatWidget({\n endpoint,\n headers,\n placeholder = 'Type a message…',\n title = 'Chat',\n welcomeMessage,\n onError,\n defaultOpen = false,\n}: ChatWidgetProps) {\n const [open, setOpen] = useState(defaultOpen);\n const [input, setInput] = useState('');\n const { messages, loading, error, send } = useChat({ endpoint, headers, onError });\n\n const id = useId();\n const panelId = `${id}-panel`;\n const titleId = `${id}-title`;\n const inputId = `${id}-input`;\n\n const toggleRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const listRef = useRef<HTMLOListElement>(null);\n\n useEffect(() => {\n if (open) inputRef.current?.focus();\n }, [open]);\n\n useEffect(() => {\n const el = listRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [messages, loading, open]);\n\n const close = () => {\n setOpen(false);\n toggleRef.current?.focus();\n };\n\n const submit = (e?: FormEvent) => {\n e?.preventDefault();\n if (!input.trim() || loading) return;\n const text = input;\n setInput('');\n void send(text);\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n }\n };\n\n const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n close();\n }\n };\n\n const last = messages[messages.length - 1];\n const waitingFirstToken = loading && last?.role !== 'assistant';\n const canSend = input.trim().length > 0 && !loading;\n\n return (\n <div style={styles.root} className=\"tr-chat-widget\">\n {open && (\n <div\n id={panelId}\n role=\"dialog\"\n aria-labelledby={titleId}\n style={styles.panel}\n className=\"tr-chat-panel\"\n onKeyDown={onPanelKeyDown}\n >\n <div style={styles.header} className=\"tr-chat-header\">\n <h2 id={titleId} style={styles.title}>\n {title}\n </h2>\n <button type=\"button\" onClick={close} aria-label=\"Close chat\" style={styles.close} className=\"tr-chat-close\">\n ×\n </button>\n </div>\n\n <ol ref={listRef} role=\"log\" aria-live=\"polite\" aria-relevant=\"additions text\" aria-busy={loading} style={styles.list} className=\"tr-chat-list\">\n {welcomeMessage && messages.length === 0 && (\n <li style={{ ...styles.bubble, ...styles.assistant }} className=\"tr-chat-msg tr-chat-msg-assistant\">\n {welcomeMessage}\n </li>\n )}\n {messages.map((m, i) => (\n <li\n key={i}\n style={{ ...styles.bubble, ...(m.role === 'user' ? styles.user : styles.assistant) }}\n className={`tr-chat-msg tr-chat-msg-${m.role}`}\n >\n <span style={styles.srOnly}>{m.role === 'user' ? 'You: ' : 'Assistant: '}</span>\n {m.content}\n </li>\n ))}\n {waitingFirstToken && (\n <li style={styles.status} className=\"tr-chat-typing\">\n Typing…\n </li>\n )}\n </ol>\n\n {error && (\n <div role=\"alert\" style={styles.error} className=\"tr-chat-error\">\n {error.message || 'Something went wrong.'}\n </div>\n )}\n\n <form onSubmit={submit} style={styles.form} className=\"tr-chat-form\">\n <label htmlFor={inputId} style={styles.srOnly}>\n Message\n </label>\n <textarea\n id={inputId}\n ref={inputRef}\n rows={1}\n value={input}\n placeholder={placeholder}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={onInputKeyDown}\n style={styles.input}\n className=\"tr-chat-input\"\n />\n <button\n type=\"submit\"\n disabled={!canSend}\n aria-label=\"Send message\"\n style={{ ...styles.send, opacity: canSend ? 1 : 0.5, cursor: canSend ? 'pointer' : 'not-allowed' }}\n className=\"tr-chat-send\"\n >\n Send\n </button>\n </form>\n </div>\n )}\n\n <button\n ref={toggleRef}\n type=\"button\"\n onClick={() => (open ? close() : setOpen(true))}\n aria-label={open ? 'Close chat' : 'Open chat'}\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n style={styles.toggle}\n className=\"tr-chat-toggle\"\n >\n {open ? (\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n ) : (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z\" />\n </svg>\n )}\n </button>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ChatMessage } from '../types';\n\nexport interface UseChatOptions {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request (e.g. a session token). Never put provider API keys here. */\n headers?: Record<string, string>;\n /** Initial conversation history. */\n initialMessages?: ChatMessage[];\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseChatResult {\n messages: ChatMessage[];\n loading: boolean;\n error: Error | null;\n /** Sends a user message and appends the assistant reply. Resolves when the reply is complete. */\n send: (message: string) => Promise<void>;\n /** Aborts the in-flight request, if any. */\n stop: () => void;\n /** Clears the conversation and the error. */\n reset: () => void;\n}\n\nexport function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult {\n const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? []);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Refs keep `send` stable and always reading the latest values.\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const headersRef = useRef(headers);\n headersRef.current = headers;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const abortRef = useRef<AbortController | null>(null);\n\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setLoading(false);\n }, []);\n\n const reset = useCallback(() => {\n stop();\n setMessages([]);\n setError(null);\n }, [stop]);\n\n const send = useCallback(\n async (message: string) => {\n const content = message.trim();\n if (!content || abortRef.current) return;\n\n // Drop empty turns (e.g. a reply that failed before its first token): backends reject them.\n const history: ChatMessage[] = [...messagesRef.current.filter((m) => m.content.trim()), { role: 'user', content }];\n setMessages(history);\n setLoading(true);\n setError(null);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n // Appends text to the assistant message at `history.length`, creating it on first call.\n const appendToReply = (text: string) => {\n setMessages((prev) => {\n const next = prev.slice();\n const current = next[history.length];\n next[history.length] = { role: 'assistant', content: (current?.content ?? '') + text };\n return next;\n });\n };\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', ...headersRef.current },\n body: JSON.stringify({ messages: history }),\n signal: controller.signal,\n });\n\n if (!res.ok) throw new Error(await readErrorMessage(res));\n\n const contentType = res.headers.get('content-type') ?? '';\n if (contentType.includes('text/event-stream')) {\n if (!res.body) throw new Error('Empty response stream');\n await readSSE(res.body, appendToReply);\n } else {\n const data: unknown = await res.json();\n const reply = data && typeof data === 'object' ? (data as { reply?: unknown }).reply : undefined;\n if (typeof reply !== 'string') throw new Error('Invalid response: expected { reply: string }');\n if (reply) appendToReply(reply);\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n onErrorRef.current?.(e);\n } finally {\n // Skip if `stop()` or a newer request already took over.\n if (abortRef.current === controller) {\n abortRef.current = null;\n setLoading(false);\n }\n }\n },\n [endpoint],\n );\n\n return { messages, loading, error, send, stop, reset };\n}\n\nasync function readErrorMessage(res: Response): Promise<string> {\n try {\n const data: unknown = await res.json();\n const msg = data && typeof data === 'object' ? (data as { error?: unknown }).error : undefined;\n if (typeof msg === 'string' && msg) return msg;\n } catch {\n // not JSON\n }\n return `Request failed with status ${res.status}`;\n}\n\n/** Minimal SSE reader. Accepts `data: {\"delta\": \"...\"}` (also `text`/`content`) or raw text; stops at `[DONE]`. */\nasync function readSSE(body: ReadableStream<Uint8Array>, onDelta: (text: string) => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n const handleEvent = (raw: string): boolean => {\n let event = 'message';\n const dataLines: string[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n if (line.startsWith('event:')) event = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return false;\n const data = dataLines.join('\\n');\n if (data === '[DONE]') return true;\n\n let parsed: unknown = data;\n try {\n parsed = JSON.parse(data);\n } catch {\n // raw text chunk\n }\n\n if (event === 'error') {\n const msg = parsed && typeof parsed === 'object' ? (parsed as { error?: unknown }).error : parsed;\n throw new Error(typeof msg === 'string' && msg ? msg : 'Stream error');\n }\n\n if (typeof parsed === 'string') onDelta(parsed);\n else if (parsed && typeof parsed === 'object') {\n const p = parsed as { delta?: unknown; text?: unknown; content?: unknown };\n const text = p.delta ?? p.text ?? p.content;\n if (typeof text === 'string') onDelta(text);\n }\n return false;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = events.pop() ?? '';\n for (const evt of events) {\n if (handleEvent(evt)) return;\n }\n }\n buffer += decoder.decode();\n if (buffer.trim()) handleEvent(buffer);\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n"],"mappings":"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","names":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client/ChatWidget.tsx","../src/client/useChat.ts"],"sourcesContent":["export * from './client';\nexport type { ChatMessage, ChatRole, ChatRequestBody, ChatResponseBody, ChatStreamChunk } from './types';\n","import {\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n} from 'react';\nimport { useChat } from './useChat';\n\n/** `CSSProperties` that also accepts CSS custom properties, e.g. `{ '--cw-accent': '#c8553d' }`. */\nexport type ChatWidgetStyle = CSSProperties & { [key: `--${string}`]: string | number | undefined };\n\nexport type ChatWidgetVariant = 'light' | 'dark';\n\nexport interface ChatWidgetProps {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request. Never put provider API keys here: they would be public. */\n headers?: Record<string, string>;\n /** Input placeholder. */\n placeholder?: string;\n /** Panel title. */\n title?: string;\n /** Message shown when the conversation is empty. Display-only: not sent to the backend. */\n welcomeMessage?: string;\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n /** Open the panel on mount. */\n defaultOpen?: boolean;\n /** Built-in palette used when a `--cw-*` property is not set. */\n variant?: ChatWidgetVariant;\n /** Extra class on the root element. Set `--cw-*` properties on it to theme the widget. */\n className?: string;\n /** Inline styles merged onto the root element. Accepts `--cw-*` properties. */\n style?: ChatWidgetStyle;\n /** Custom launcher icon, shown while the panel is closed. */\n toggleIcon?: ReactNode;\n}\n\nconst FONT = 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n/** Fallback values for every `--cw-*` property. A property set by the consumer always wins. */\nconst LIGHT = {\n font: FONT,\n text: '#111827',\n muted: '#6b7280',\n 'panel-bg': '#ffffff',\n 'list-bg': '#f9fafb',\n border: '#e5e7eb',\n accent: '#2563eb',\n 'accent-contrast': '#ffffff',\n 'bubble-bg': '#ffffff',\n 'bubble-text': '#111827',\n 'input-bg': '#ffffff',\n 'input-border': '#d1d5db',\n 'error-bg': '#fef2f2',\n 'error-text': '#b91c1c',\n shadow: '0 12px 40px rgba(0,0,0,0.18)',\n 'toggle-shadow': '0 6px 20px rgba(0,0,0,0.2)',\n radius: '12px',\n 'radius-sm': '8px',\n};\n\ntype Theme = typeof LIGHT;\n\nconst DARK: Theme = {\n ...LIGHT,\n text: '#e5e0db',\n muted: '#8a8580',\n 'panel-bg': '#0c0c0c',\n 'list-bg': '#0a0a0a',\n border: 'rgba(255,255,255,0.08)',\n 'bubble-bg': 'rgba(255,255,255,0.04)',\n 'bubble-text': '#e5e0db',\n 'input-bg': 'rgba(255,255,255,0.03)',\n 'input-border': 'rgba(255,255,255,0.08)',\n 'error-bg': 'rgba(185,28,28,0.15)',\n 'error-text': '#fca5a5',\n};\n\nfunction createStyles(theme: Theme) {\n const v = (name: keyof Theme) => `var(--cw-${name}, ${theme[name]})`;\n const accent = v('accent');\n const accentContrast = v('accent-contrast');\n const border = `1px solid ${v('border')}`;\n\n return {\n root: {\n position: 'fixed',\n right: 20,\n bottom: 20,\n zIndex: 2147483000,\n fontFamily: v('font'),\n fontSize: 14,\n lineHeight: 1.45,\n color: v('text'),\n boxSizing: 'border-box',\n },\n toggle: {\n width: 56,\n height: 56,\n borderRadius: '50%',\n border: 'none',\n background: accent,\n color: accentContrast,\n cursor: 'pointer',\n boxShadow: v('toggle-shadow'),\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n marginLeft: 'auto',\n padding: 0,\n },\n panel: {\n width: 'min(370px, calc(100vw - 40px))',\n height: 'min(540px, calc(100vh - 110px))',\n marginBottom: 12,\n background: v('panel-bg'),\n borderRadius: v('radius'),\n boxShadow: v('shadow'),\n border,\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n background: `var(--cw-header-bg, ${accent})`,\n color: `var(--cw-header-text, ${accentContrast})`,\n borderBottom: 'var(--cw-header-border, none)',\n },\n title: { margin: 0, fontSize: 15, fontWeight: 600, fontFamily: 'var(--cw-title-font, inherit)' },\n close: {\n background: 'transparent',\n border: 'none',\n color: 'inherit',\n cursor: 'pointer',\n fontSize: 20,\n lineHeight: 1,\n padding: 4,\n },\n list: {\n flex: 1,\n overflowY: 'auto',\n padding: 14,\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n background: v('list-bg'),\n margin: 0,\n listStyle: 'none',\n },\n bubble: {\n maxWidth: '85%',\n padding: '8px 12px',\n borderRadius: v('radius'),\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n },\n user: { alignSelf: 'flex-end', background: accent, color: accentContrast, borderBottomRightRadius: 4 },\n assistant: {\n alignSelf: 'flex-start',\n background: v('bubble-bg'),\n color: v('bubble-text'),\n border,\n borderBottomLeftRadius: 4,\n },\n status: { alignSelf: 'flex-start', color: v('muted'), fontStyle: 'italic', padding: '0 4px' },\n error: {\n margin: '0 14px 8px',\n padding: '8px 10px',\n borderRadius: v('radius-sm'),\n background: v('error-bg'),\n color: v('error-text'),\n fontSize: 13,\n },\n form: { display: 'flex', gap: 8, padding: 10, borderTop: border, background: v('panel-bg') },\n input: {\n flex: 1,\n resize: 'none',\n border: `1px solid ${v('input-border')}`,\n borderRadius: v('radius-sm'),\n padding: '8px 10px',\n font: 'inherit',\n color: 'inherit',\n background: v('input-bg'),\n minHeight: 38,\n maxHeight: 120,\n boxSizing: 'border-box',\n },\n send: {\n border: 'none',\n borderRadius: v('radius-sm'),\n background: accent,\n color: accentContrast,\n padding: '0 14px',\n font: 'inherit',\n fontWeight: 600,\n cursor: 'pointer',\n },\n srOnly: {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n whiteSpace: 'nowrap',\n border: 0,\n },\n } satisfies Record<string, CSSProperties>;\n}\n\nconst STYLES: Record<ChatWidgetVariant, ReturnType<typeof createStyles>> = {\n light: createStyles(LIGHT),\n dark: createStyles(DARK),\n};\n\nexport function ChatWidget({\n endpoint,\n headers,\n placeholder = 'Ask me anything…',\n title = 'Chat with me',\n welcomeMessage,\n onError,\n defaultOpen = false,\n variant = 'light',\n className,\n style,\n toggleIcon,\n}: ChatWidgetProps) {\n const styles = STYLES[variant] ?? STYLES.light;\n const [open, setOpen] = useState(defaultOpen);\n const [input, setInput] = useState('');\n const { messages, loading, error, send } = useChat({ endpoint, headers, onError });\n\n const id = useId();\n const panelId = `${id}-panel`;\n const titleId = `${id}-title`;\n const inputId = `${id}-input`;\n\n const toggleRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const listRef = useRef<HTMLOListElement>(null);\n\n useEffect(() => {\n if (open) inputRef.current?.focus();\n }, [open]);\n\n useEffect(() => {\n const el = listRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [messages, loading, open]);\n\n const close = () => {\n setOpen(false);\n toggleRef.current?.focus();\n };\n\n const submit = (e?: FormEvent) => {\n e?.preventDefault();\n if (!input.trim() || loading) return;\n const text = input;\n setInput('');\n void send(text);\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n }\n };\n\n const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n close();\n }\n };\n\n const last = messages[messages.length - 1];\n const waitingFirstToken = loading && last?.role !== 'assistant';\n const canSend = input.trim().length > 0 && !loading;\n\n return (\n <div style={{ ...styles.root, ...style }} className={className ? `tr-chat-widget ${className}` : 'tr-chat-widget'}>\n {open && (\n <div\n id={panelId}\n role=\"dialog\"\n aria-labelledby={titleId}\n style={styles.panel}\n className=\"tr-chat-panel\"\n onKeyDown={onPanelKeyDown}\n >\n <div style={styles.header} className=\"tr-chat-header\">\n <h2 id={titleId} style={styles.title}>\n {title}\n </h2>\n <button type=\"button\" onClick={close} aria-label=\"Close chat\" style={styles.close} className=\"tr-chat-close\">\n ×\n </button>\n </div>\n\n <ol ref={listRef} role=\"log\" aria-live=\"polite\" aria-relevant=\"additions text\" aria-busy={loading} style={styles.list} className=\"tr-chat-list\">\n {welcomeMessage && messages.length === 0 && (\n <li style={{ ...styles.bubble, ...styles.assistant }} className=\"tr-chat-msg tr-chat-msg-assistant\">\n {welcomeMessage}\n </li>\n )}\n {messages.map((m, i) => (\n <li\n key={i}\n style={{ ...styles.bubble, ...(m.role === 'user' ? styles.user : styles.assistant) }}\n className={`tr-chat-msg tr-chat-msg-${m.role}`}\n >\n <span style={styles.srOnly}>{m.role === 'user' ? 'You: ' : 'Assistant: '}</span>\n {m.content}\n </li>\n ))}\n {waitingFirstToken && (\n <li style={styles.status} className=\"tr-chat-typing\">\n Typing…\n </li>\n )}\n </ol>\n\n {error && (\n <div role=\"alert\" style={styles.error} className=\"tr-chat-error\">\n {error.message || 'Something went wrong.'}\n </div>\n )}\n\n <form onSubmit={submit} style={styles.form} className=\"tr-chat-form\">\n <label htmlFor={inputId} style={styles.srOnly}>\n Message\n </label>\n <textarea\n id={inputId}\n ref={inputRef}\n rows={1}\n value={input}\n placeholder={placeholder}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={onInputKeyDown}\n style={styles.input}\n className=\"tr-chat-input\"\n />\n <button\n type=\"submit\"\n disabled={!canSend}\n aria-label=\"Send message\"\n style={{ ...styles.send, opacity: canSend ? 1 : 0.5, cursor: canSend ? 'pointer' : 'not-allowed' }}\n className=\"tr-chat-send\"\n >\n Send\n </button>\n </form>\n </div>\n )}\n\n <button\n ref={toggleRef}\n type=\"button\"\n onClick={() => (open ? close() : setOpen(true))}\n aria-label={open ? 'Close chat' : 'Open chat'}\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n style={styles.toggle}\n className=\"tr-chat-toggle\"\n >\n {open ? (\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n ) : toggleIcon !== undefined ? (\n toggleIcon\n ) : (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z\" />\n </svg>\n )}\n </button>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ChatMessage } from '../types';\n\nexport interface UseChatOptions {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request (e.g. a session token). Never put provider API keys here. */\n headers?: Record<string, string>;\n /** Initial conversation history. */\n initialMessages?: ChatMessage[];\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseChatResult {\n messages: ChatMessage[];\n loading: boolean;\n error: Error | null;\n /** Sends a user message and appends the assistant reply. Resolves when the reply is complete. */\n send: (message: string) => Promise<void>;\n /** Aborts the in-flight request, if any. */\n stop: () => void;\n /** Clears the conversation and the error. */\n reset: () => void;\n}\n\nexport function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult {\n const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? []);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Refs keep `send` stable and always reading the latest values.\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const headersRef = useRef(headers);\n headersRef.current = headers;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const abortRef = useRef<AbortController | null>(null);\n\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setLoading(false);\n }, []);\n\n const reset = useCallback(() => {\n stop();\n setMessages([]);\n setError(null);\n }, [stop]);\n\n const send = useCallback(\n async (message: string) => {\n const content = message.trim();\n if (!content || abortRef.current) return;\n\n // Drop empty turns (e.g. a reply that failed before its first token): backends reject them.\n const history: ChatMessage[] = [...messagesRef.current.filter((m) => m.content.trim()), { role: 'user', content }];\n setMessages(history);\n setLoading(true);\n setError(null);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n // Appends text to the assistant message at `history.length`, creating it on first call.\n const appendToReply = (text: string) => {\n setMessages((prev) => {\n const next = prev.slice();\n const current = next[history.length];\n next[history.length] = { role: 'assistant', content: (current?.content ?? '') + text };\n return next;\n });\n };\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', ...headersRef.current },\n body: JSON.stringify({ messages: history }),\n signal: controller.signal,\n });\n\n if (!res.ok) throw new Error(await readErrorMessage(res));\n\n const contentType = res.headers.get('content-type') ?? '';\n if (contentType.includes('text/event-stream')) {\n if (!res.body) throw new Error('Empty response stream');\n await readSSE(res.body, appendToReply);\n } else {\n const data: unknown = await res.json();\n const reply = data && typeof data === 'object' ? (data as { reply?: unknown }).reply : undefined;\n if (typeof reply !== 'string') throw new Error('Invalid response: expected { reply: string }');\n if (reply) appendToReply(reply);\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n onErrorRef.current?.(e);\n } finally {\n // Skip if `stop()` or a newer request already took over.\n if (abortRef.current === controller) {\n abortRef.current = null;\n setLoading(false);\n }\n }\n },\n [endpoint],\n );\n\n return { messages, loading, error, send, stop, reset };\n}\n\nasync function readErrorMessage(res: Response): Promise<string> {\n try {\n const data: unknown = await res.json();\n const msg = data && typeof data === 'object' ? (data as { error?: unknown }).error : undefined;\n if (typeof msg === 'string' && msg) return msg;\n } catch {\n // not JSON\n }\n return `Request failed with status ${res.status}`;\n}\n\n/** Minimal SSE reader. Accepts `data: {\"delta\": \"...\"}` (also `text`/`content`) or raw text; stops at `[DONE]`. */\nasync function readSSE(body: ReadableStream<Uint8Array>, onDelta: (text: string) => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n const handleEvent = (raw: string): boolean => {\n let event = 'message';\n const dataLines: string[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n if (line.startsWith('event:')) event = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return false;\n const data = dataLines.join('\\n');\n if (data === '[DONE]') return true;\n\n let parsed: unknown = data;\n try {\n parsed = JSON.parse(data);\n } catch {\n // raw text chunk\n }\n\n if (event === 'error') {\n const msg = parsed && typeof parsed === 'object' ? (parsed as { error?: unknown }).error : parsed;\n throw new Error(typeof msg === 'string' && msg ? msg : 'Stream error');\n }\n\n if (typeof parsed === 'string') onDelta(parsed);\n else if (parsed && typeof parsed === 'object') {\n const p = parsed as { delta?: unknown; text?: unknown; content?: unknown };\n const text = p.delta ?? p.text ?? p.content;\n if (typeof text === 'string') onDelta(text);\n }\n return false;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = events.pop() ?? '';\n for (const evt of events) {\n if (handleEvent(evt)) return;\n }\n }\n buffer += decoder.decode();\n if (buffer.trim()) handleEvent(buffer);\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n"],"mappings":"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","names":["import_react"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
3
|
|
|
4
|
+
/** `CSSProperties` that also accepts CSS custom properties, e.g. `{ '--cw-accent': '#c8553d' }`. */
|
|
5
|
+
type ChatWidgetStyle = CSSProperties & {
|
|
6
|
+
[key: `--${string}`]: string | number | undefined;
|
|
7
|
+
};
|
|
8
|
+
type ChatWidgetVariant = 'light' | 'dark';
|
|
3
9
|
interface ChatWidgetProps {
|
|
4
10
|
/** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */
|
|
5
11
|
endpoint: string;
|
|
@@ -15,8 +21,16 @@ interface ChatWidgetProps {
|
|
|
15
21
|
onError?: (error: Error) => void;
|
|
16
22
|
/** Open the panel on mount. */
|
|
17
23
|
defaultOpen?: boolean;
|
|
24
|
+
/** Built-in palette used when a `--cw-*` property is not set. */
|
|
25
|
+
variant?: ChatWidgetVariant;
|
|
26
|
+
/** Extra class on the root element. Set `--cw-*` properties on it to theme the widget. */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Inline styles merged onto the root element. Accepts `--cw-*` properties. */
|
|
29
|
+
style?: ChatWidgetStyle;
|
|
30
|
+
/** Custom launcher icon, shown while the panel is closed. */
|
|
31
|
+
toggleIcon?: ReactNode;
|
|
18
32
|
}
|
|
19
|
-
declare function ChatWidget({ endpoint, headers, placeholder, title, welcomeMessage, onError, defaultOpen, }: ChatWidgetProps): react.JSX.Element;
|
|
33
|
+
declare function ChatWidget({ endpoint, headers, placeholder, title, welcomeMessage, onError, defaultOpen, variant, className, style, toggleIcon, }: ChatWidgetProps): react.JSX.Element;
|
|
20
34
|
|
|
21
35
|
type ChatRole = 'user' | 'assistant';
|
|
22
36
|
interface ChatMessage {
|
|
@@ -62,4 +76,4 @@ interface UseChatResult {
|
|
|
62
76
|
}
|
|
63
77
|
declare function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult;
|
|
64
78
|
|
|
65
|
-
export { type ChatMessage, type ChatRequestBody, type ChatResponseBody, type ChatRole, type ChatStreamChunk, ChatWidget, type ChatWidgetProps, type UseChatOptions, type UseChatResult, useChat };
|
|
79
|
+
export { type ChatMessage, type ChatRequestBody, type ChatResponseBody, type ChatRole, type ChatStreamChunk, ChatWidget, type ChatWidgetProps, type ChatWidgetStyle, type ChatWidgetVariant, type UseChatOptions, type UseChatResult, useChat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
3
|
|
|
4
|
+
/** `CSSProperties` that also accepts CSS custom properties, e.g. `{ '--cw-accent': '#c8553d' }`. */
|
|
5
|
+
type ChatWidgetStyle = CSSProperties & {
|
|
6
|
+
[key: `--${string}`]: string | number | undefined;
|
|
7
|
+
};
|
|
8
|
+
type ChatWidgetVariant = 'light' | 'dark';
|
|
3
9
|
interface ChatWidgetProps {
|
|
4
10
|
/** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */
|
|
5
11
|
endpoint: string;
|
|
@@ -15,8 +21,16 @@ interface ChatWidgetProps {
|
|
|
15
21
|
onError?: (error: Error) => void;
|
|
16
22
|
/** Open the panel on mount. */
|
|
17
23
|
defaultOpen?: boolean;
|
|
24
|
+
/** Built-in palette used when a `--cw-*` property is not set. */
|
|
25
|
+
variant?: ChatWidgetVariant;
|
|
26
|
+
/** Extra class on the root element. Set `--cw-*` properties on it to theme the widget. */
|
|
27
|
+
className?: string;
|
|
28
|
+
/** Inline styles merged onto the root element. Accepts `--cw-*` properties. */
|
|
29
|
+
style?: ChatWidgetStyle;
|
|
30
|
+
/** Custom launcher icon, shown while the panel is closed. */
|
|
31
|
+
toggleIcon?: ReactNode;
|
|
18
32
|
}
|
|
19
|
-
declare function ChatWidget({ endpoint, headers, placeholder, title, welcomeMessage, onError, defaultOpen, }: ChatWidgetProps): react.JSX.Element;
|
|
33
|
+
declare function ChatWidget({ endpoint, headers, placeholder, title, welcomeMessage, onError, defaultOpen, variant, className, style, toggleIcon, }: ChatWidgetProps): react.JSX.Element;
|
|
20
34
|
|
|
21
35
|
type ChatRole = 'user' | 'assistant';
|
|
22
36
|
interface ChatMessage {
|
|
@@ -62,4 +76,4 @@ interface UseChatResult {
|
|
|
62
76
|
}
|
|
63
77
|
declare function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult;
|
|
64
78
|
|
|
65
|
-
export { type ChatMessage, type ChatRequestBody, type ChatResponseBody, type ChatRole, type ChatStreamChunk, ChatWidget, type ChatWidgetProps, type UseChatOptions, type UseChatResult, useChat };
|
|
79
|
+
export { type ChatMessage, type ChatRequestBody, type ChatResponseBody, type ChatRole, type ChatStreamChunk, ChatWidget, type ChatWidgetProps, type ChatWidgetStyle, type ChatWidgetVariant, type UseChatOptions, type UseChatResult, useChat };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/client/ChatWidget.tsx
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useEffect as useEffect2,
|
|
6
|
+
useId,
|
|
7
|
+
useRef as useRef2,
|
|
8
|
+
useState as useState2
|
|
9
|
+
} from "react";
|
|
5
10
|
|
|
6
11
|
// src/client/useChat.ts
|
|
7
12
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -140,139 +145,194 @@ async function readSSE(body, onDelta) {
|
|
|
140
145
|
|
|
141
146
|
// src/client/ChatWidget.tsx
|
|
142
147
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
143
|
-
var ACCENT = "#2563eb";
|
|
144
148
|
var FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
145
|
-
var
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
149
|
+
var LIGHT = {
|
|
150
|
+
font: FONT,
|
|
151
|
+
text: "#111827",
|
|
152
|
+
muted: "#6b7280",
|
|
153
|
+
"panel-bg": "#ffffff",
|
|
154
|
+
"list-bg": "#f9fafb",
|
|
155
|
+
border: "#e5e7eb",
|
|
156
|
+
accent: "#2563eb",
|
|
157
|
+
"accent-contrast": "#ffffff",
|
|
158
|
+
"bubble-bg": "#ffffff",
|
|
159
|
+
"bubble-text": "#111827",
|
|
160
|
+
"input-bg": "#ffffff",
|
|
161
|
+
"input-border": "#d1d5db",
|
|
162
|
+
"error-bg": "#fef2f2",
|
|
163
|
+
"error-text": "#b91c1c",
|
|
164
|
+
shadow: "0 12px 40px rgba(0,0,0,0.18)",
|
|
165
|
+
"toggle-shadow": "0 6px 20px rgba(0,0,0,0.2)",
|
|
166
|
+
radius: "12px",
|
|
167
|
+
"radius-sm": "8px"
|
|
168
|
+
};
|
|
169
|
+
var DARK = {
|
|
170
|
+
...LIGHT,
|
|
171
|
+
text: "#e5e0db",
|
|
172
|
+
muted: "#8a8580",
|
|
173
|
+
"panel-bg": "#0c0c0c",
|
|
174
|
+
"list-bg": "#0a0a0a",
|
|
175
|
+
border: "rgba(255,255,255,0.08)",
|
|
176
|
+
"bubble-bg": "rgba(255,255,255,0.04)",
|
|
177
|
+
"bubble-text": "#e5e0db",
|
|
178
|
+
"input-bg": "rgba(255,255,255,0.03)",
|
|
179
|
+
"input-border": "rgba(255,255,255,0.08)",
|
|
180
|
+
"error-bg": "rgba(185,28,28,0.15)",
|
|
181
|
+
"error-text": "#fca5a5"
|
|
182
|
+
};
|
|
183
|
+
function createStyles(theme) {
|
|
184
|
+
const v = (name) => `var(--cw-${name}, ${theme[name]})`;
|
|
185
|
+
const accent = v("accent");
|
|
186
|
+
const accentContrast = v("accent-contrast");
|
|
187
|
+
const border = `1px solid ${v("border")}`;
|
|
188
|
+
return {
|
|
189
|
+
root: {
|
|
190
|
+
position: "fixed",
|
|
191
|
+
right: 20,
|
|
192
|
+
bottom: 20,
|
|
193
|
+
zIndex: 2147483e3,
|
|
194
|
+
fontFamily: v("font"),
|
|
195
|
+
fontSize: 14,
|
|
196
|
+
lineHeight: 1.45,
|
|
197
|
+
color: v("text"),
|
|
198
|
+
boxSizing: "border-box"
|
|
199
|
+
},
|
|
200
|
+
toggle: {
|
|
201
|
+
width: 56,
|
|
202
|
+
height: 56,
|
|
203
|
+
borderRadius: "50%",
|
|
204
|
+
border: "none",
|
|
205
|
+
background: accent,
|
|
206
|
+
color: accentContrast,
|
|
207
|
+
cursor: "pointer",
|
|
208
|
+
boxShadow: v("toggle-shadow"),
|
|
209
|
+
display: "flex",
|
|
210
|
+
alignItems: "center",
|
|
211
|
+
justifyContent: "center",
|
|
212
|
+
marginLeft: "auto",
|
|
213
|
+
padding: 0
|
|
214
|
+
},
|
|
215
|
+
panel: {
|
|
216
|
+
width: "min(370px, calc(100vw - 40px))",
|
|
217
|
+
height: "min(540px, calc(100vh - 110px))",
|
|
218
|
+
marginBottom: 12,
|
|
219
|
+
background: v("panel-bg"),
|
|
220
|
+
borderRadius: v("radius"),
|
|
221
|
+
boxShadow: v("shadow"),
|
|
222
|
+
border,
|
|
223
|
+
display: "flex",
|
|
224
|
+
flexDirection: "column",
|
|
225
|
+
overflow: "hidden"
|
|
226
|
+
},
|
|
227
|
+
header: {
|
|
228
|
+
display: "flex",
|
|
229
|
+
alignItems: "center",
|
|
230
|
+
justifyContent: "space-between",
|
|
231
|
+
padding: "12px 14px",
|
|
232
|
+
background: `var(--cw-header-bg, ${accent})`,
|
|
233
|
+
color: `var(--cw-header-text, ${accentContrast})`,
|
|
234
|
+
borderBottom: "var(--cw-header-border, none)"
|
|
235
|
+
},
|
|
236
|
+
title: { margin: 0, fontSize: 15, fontWeight: 600, fontFamily: "var(--cw-title-font, inherit)" },
|
|
237
|
+
close: {
|
|
238
|
+
background: "transparent",
|
|
239
|
+
border: "none",
|
|
240
|
+
color: "inherit",
|
|
241
|
+
cursor: "pointer",
|
|
242
|
+
fontSize: 20,
|
|
243
|
+
lineHeight: 1,
|
|
244
|
+
padding: 4
|
|
245
|
+
},
|
|
246
|
+
list: {
|
|
247
|
+
flex: 1,
|
|
248
|
+
overflowY: "auto",
|
|
249
|
+
padding: 14,
|
|
250
|
+
display: "flex",
|
|
251
|
+
flexDirection: "column",
|
|
252
|
+
gap: 8,
|
|
253
|
+
background: v("list-bg"),
|
|
254
|
+
margin: 0,
|
|
255
|
+
listStyle: "none"
|
|
256
|
+
},
|
|
257
|
+
bubble: {
|
|
258
|
+
maxWidth: "85%",
|
|
259
|
+
padding: "8px 12px",
|
|
260
|
+
borderRadius: v("radius"),
|
|
261
|
+
whiteSpace: "pre-wrap",
|
|
262
|
+
wordBreak: "break-word"
|
|
263
|
+
},
|
|
264
|
+
user: { alignSelf: "flex-end", background: accent, color: accentContrast, borderBottomRightRadius: 4 },
|
|
265
|
+
assistant: {
|
|
266
|
+
alignSelf: "flex-start",
|
|
267
|
+
background: v("bubble-bg"),
|
|
268
|
+
color: v("bubble-text"),
|
|
269
|
+
border,
|
|
270
|
+
borderBottomLeftRadius: 4
|
|
271
|
+
},
|
|
272
|
+
status: { alignSelf: "flex-start", color: v("muted"), fontStyle: "italic", padding: "0 4px" },
|
|
273
|
+
error: {
|
|
274
|
+
margin: "0 14px 8px",
|
|
275
|
+
padding: "8px 10px",
|
|
276
|
+
borderRadius: v("radius-sm"),
|
|
277
|
+
background: v("error-bg"),
|
|
278
|
+
color: v("error-text"),
|
|
279
|
+
fontSize: 13
|
|
280
|
+
},
|
|
281
|
+
form: { display: "flex", gap: 8, padding: 10, borderTop: border, background: v("panel-bg") },
|
|
282
|
+
input: {
|
|
283
|
+
flex: 1,
|
|
284
|
+
resize: "none",
|
|
285
|
+
border: `1px solid ${v("input-border")}`,
|
|
286
|
+
borderRadius: v("radius-sm"),
|
|
287
|
+
padding: "8px 10px",
|
|
288
|
+
font: "inherit",
|
|
289
|
+
color: "inherit",
|
|
290
|
+
background: v("input-bg"),
|
|
291
|
+
minHeight: 38,
|
|
292
|
+
maxHeight: 120,
|
|
293
|
+
boxSizing: "border-box"
|
|
294
|
+
},
|
|
295
|
+
send: {
|
|
296
|
+
border: "none",
|
|
297
|
+
borderRadius: v("radius-sm"),
|
|
298
|
+
background: accent,
|
|
299
|
+
color: accentContrast,
|
|
300
|
+
padding: "0 14px",
|
|
301
|
+
font: "inherit",
|
|
302
|
+
fontWeight: 600,
|
|
303
|
+
cursor: "pointer"
|
|
304
|
+
},
|
|
305
|
+
srOnly: {
|
|
306
|
+
position: "absolute",
|
|
307
|
+
width: 1,
|
|
308
|
+
height: 1,
|
|
309
|
+
padding: 0,
|
|
310
|
+
margin: -1,
|
|
311
|
+
overflow: "hidden",
|
|
312
|
+
clip: "rect(0,0,0,0)",
|
|
313
|
+
whiteSpace: "nowrap",
|
|
314
|
+
border: 0
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
var STYLES = {
|
|
319
|
+
light: createStyles(LIGHT),
|
|
320
|
+
dark: createStyles(DARK)
|
|
266
321
|
};
|
|
267
322
|
function ChatWidget({
|
|
268
323
|
endpoint,
|
|
269
324
|
headers,
|
|
270
|
-
placeholder = "
|
|
271
|
-
title = "Chat",
|
|
325
|
+
placeholder = "Ask me anything\u2026",
|
|
326
|
+
title = "Chat with me",
|
|
272
327
|
welcomeMessage,
|
|
273
328
|
onError,
|
|
274
|
-
defaultOpen = false
|
|
329
|
+
defaultOpen = false,
|
|
330
|
+
variant = "light",
|
|
331
|
+
className,
|
|
332
|
+
style,
|
|
333
|
+
toggleIcon
|
|
275
334
|
}) {
|
|
335
|
+
const styles = STYLES[variant] ?? STYLES.light;
|
|
276
336
|
const [open, setOpen] = useState2(defaultOpen);
|
|
277
337
|
const [input, setInput] = useState2("");
|
|
278
338
|
const { messages, loading, error, send } = useChat({ endpoint, headers, onError });
|
|
@@ -316,7 +376,7 @@ function ChatWidget({
|
|
|
316
376
|
const last = messages[messages.length - 1];
|
|
317
377
|
const waitingFirstToken = loading && last?.role !== "assistant";
|
|
318
378
|
const canSend = input.trim().length > 0 && !loading;
|
|
319
|
-
return /* @__PURE__ */ jsxs("div", { style: styles.root, className: "tr-chat-widget", children: [
|
|
379
|
+
return /* @__PURE__ */ jsxs("div", { style: { ...styles.root, ...style }, className: className ? `tr-chat-widget ${className}` : "tr-chat-widget", children: [
|
|
320
380
|
open && /* @__PURE__ */ jsxs(
|
|
321
381
|
"div",
|
|
322
382
|
{
|
|
@@ -390,7 +450,7 @@ function ChatWidget({
|
|
|
390
450
|
"aria-controls": open ? panelId : void 0,
|
|
391
451
|
style: styles.toggle,
|
|
392
452
|
className: "tr-chat-toggle",
|
|
393
|
-
children: open ? /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) : /* @__PURE__ */ jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z" }) })
|
|
453
|
+
children: open ? /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) : toggleIcon !== void 0 ? toggleIcon : /* @__PURE__ */ jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z" }) })
|
|
394
454
|
}
|
|
395
455
|
)
|
|
396
456
|
] });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/ChatWidget.tsx","../src/client/useChat.ts"],"sourcesContent":["import { useEffect, useId, useRef, useState, type CSSProperties, type FormEvent, type KeyboardEvent } from 'react';\nimport { useChat } from './useChat';\n\nexport interface ChatWidgetProps {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request. Never put provider API keys here: they would be public. */\n headers?: Record<string, string>;\n /** Input placeholder. */\n placeholder?: string;\n /** Panel title. */\n title?: string;\n /** Message shown when the conversation is empty. Display-only: not sent to the backend. */\n welcomeMessage?: string;\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n /** Open the panel on mount. */\n defaultOpen?: boolean;\n}\n\nconst ACCENT = '#2563eb';\nconst FONT = 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\nconst styles = {\n root: {\n position: 'fixed',\n right: 20,\n bottom: 20,\n zIndex: 2147483000,\n fontFamily: FONT,\n fontSize: 14,\n lineHeight: 1.45,\n color: '#111827',\n boxSizing: 'border-box',\n },\n toggle: {\n width: 56,\n height: 56,\n borderRadius: '50%',\n border: 'none',\n background: ACCENT,\n color: '#fff',\n cursor: 'pointer',\n boxShadow: '0 6px 20px rgba(0,0,0,0.2)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n marginLeft: 'auto',\n padding: 0,\n },\n panel: {\n width: 'min(370px, calc(100vw - 40px))',\n height: 'min(540px, calc(100vh - 110px))',\n marginBottom: 12,\n background: '#fff',\n borderRadius: 12,\n boxShadow: '0 12px 40px rgba(0,0,0,0.18)',\n border: '1px solid #e5e7eb',\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n background: ACCENT,\n color: '#fff',\n },\n title: { margin: 0, fontSize: 15, fontWeight: 600 },\n close: {\n background: 'transparent',\n border: 'none',\n color: '#fff',\n cursor: 'pointer',\n fontSize: 20,\n lineHeight: 1,\n padding: 4,\n },\n list: {\n flex: 1,\n overflowY: 'auto',\n padding: 14,\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n background: '#f9fafb',\n margin: 0,\n listStyle: 'none',\n },\n bubble: {\n maxWidth: '85%',\n padding: '8px 12px',\n borderRadius: 12,\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n },\n user: { alignSelf: 'flex-end', background: ACCENT, color: '#fff', borderBottomRightRadius: 4 },\n assistant: { alignSelf: 'flex-start', background: '#fff', border: '1px solid #e5e7eb', borderBottomLeftRadius: 4 },\n status: { alignSelf: 'flex-start', color: '#6b7280', fontStyle: 'italic', padding: '0 4px' },\n error: {\n margin: '0 14px 8px',\n padding: '8px 10px',\n borderRadius: 8,\n background: '#fef2f2',\n color: '#b91c1c',\n fontSize: 13,\n },\n form: { display: 'flex', gap: 8, padding: 10, borderTop: '1px solid #e5e7eb', background: '#fff' },\n input: {\n flex: 1,\n resize: 'none',\n border: '1px solid #d1d5db',\n borderRadius: 8,\n padding: '8px 10px',\n font: 'inherit',\n color: 'inherit',\n background: '#fff',\n minHeight: 38,\n maxHeight: 120,\n boxSizing: 'border-box',\n },\n send: {\n border: 'none',\n borderRadius: 8,\n background: ACCENT,\n color: '#fff',\n padding: '0 14px',\n font: 'inherit',\n fontWeight: 600,\n cursor: 'pointer',\n },\n srOnly: {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n whiteSpace: 'nowrap',\n border: 0,\n },\n} satisfies Record<string, CSSProperties>;\n\nexport function ChatWidget({\n endpoint,\n headers,\n placeholder = 'Type a message…',\n title = 'Chat',\n welcomeMessage,\n onError,\n defaultOpen = false,\n}: ChatWidgetProps) {\n const [open, setOpen] = useState(defaultOpen);\n const [input, setInput] = useState('');\n const { messages, loading, error, send } = useChat({ endpoint, headers, onError });\n\n const id = useId();\n const panelId = `${id}-panel`;\n const titleId = `${id}-title`;\n const inputId = `${id}-input`;\n\n const toggleRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const listRef = useRef<HTMLOListElement>(null);\n\n useEffect(() => {\n if (open) inputRef.current?.focus();\n }, [open]);\n\n useEffect(() => {\n const el = listRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [messages, loading, open]);\n\n const close = () => {\n setOpen(false);\n toggleRef.current?.focus();\n };\n\n const submit = (e?: FormEvent) => {\n e?.preventDefault();\n if (!input.trim() || loading) return;\n const text = input;\n setInput('');\n void send(text);\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n }\n };\n\n const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n close();\n }\n };\n\n const last = messages[messages.length - 1];\n const waitingFirstToken = loading && last?.role !== 'assistant';\n const canSend = input.trim().length > 0 && !loading;\n\n return (\n <div style={styles.root} className=\"tr-chat-widget\">\n {open && (\n <div\n id={panelId}\n role=\"dialog\"\n aria-labelledby={titleId}\n style={styles.panel}\n className=\"tr-chat-panel\"\n onKeyDown={onPanelKeyDown}\n >\n <div style={styles.header} className=\"tr-chat-header\">\n <h2 id={titleId} style={styles.title}>\n {title}\n </h2>\n <button type=\"button\" onClick={close} aria-label=\"Close chat\" style={styles.close} className=\"tr-chat-close\">\n ×\n </button>\n </div>\n\n <ol ref={listRef} role=\"log\" aria-live=\"polite\" aria-relevant=\"additions text\" aria-busy={loading} style={styles.list} className=\"tr-chat-list\">\n {welcomeMessage && messages.length === 0 && (\n <li style={{ ...styles.bubble, ...styles.assistant }} className=\"tr-chat-msg tr-chat-msg-assistant\">\n {welcomeMessage}\n </li>\n )}\n {messages.map((m, i) => (\n <li\n key={i}\n style={{ ...styles.bubble, ...(m.role === 'user' ? styles.user : styles.assistant) }}\n className={`tr-chat-msg tr-chat-msg-${m.role}`}\n >\n <span style={styles.srOnly}>{m.role === 'user' ? 'You: ' : 'Assistant: '}</span>\n {m.content}\n </li>\n ))}\n {waitingFirstToken && (\n <li style={styles.status} className=\"tr-chat-typing\">\n Typing…\n </li>\n )}\n </ol>\n\n {error && (\n <div role=\"alert\" style={styles.error} className=\"tr-chat-error\">\n {error.message || 'Something went wrong.'}\n </div>\n )}\n\n <form onSubmit={submit} style={styles.form} className=\"tr-chat-form\">\n <label htmlFor={inputId} style={styles.srOnly}>\n Message\n </label>\n <textarea\n id={inputId}\n ref={inputRef}\n rows={1}\n value={input}\n placeholder={placeholder}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={onInputKeyDown}\n style={styles.input}\n className=\"tr-chat-input\"\n />\n <button\n type=\"submit\"\n disabled={!canSend}\n aria-label=\"Send message\"\n style={{ ...styles.send, opacity: canSend ? 1 : 0.5, cursor: canSend ? 'pointer' : 'not-allowed' }}\n className=\"tr-chat-send\"\n >\n Send\n </button>\n </form>\n </div>\n )}\n\n <button\n ref={toggleRef}\n type=\"button\"\n onClick={() => (open ? close() : setOpen(true))}\n aria-label={open ? 'Close chat' : 'Open chat'}\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n style={styles.toggle}\n className=\"tr-chat-toggle\"\n >\n {open ? (\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n ) : (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z\" />\n </svg>\n )}\n </button>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ChatMessage } from '../types';\n\nexport interface UseChatOptions {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request (e.g. a session token). Never put provider API keys here. */\n headers?: Record<string, string>;\n /** Initial conversation history. */\n initialMessages?: ChatMessage[];\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseChatResult {\n messages: ChatMessage[];\n loading: boolean;\n error: Error | null;\n /** Sends a user message and appends the assistant reply. Resolves when the reply is complete. */\n send: (message: string) => Promise<void>;\n /** Aborts the in-flight request, if any. */\n stop: () => void;\n /** Clears the conversation and the error. */\n reset: () => void;\n}\n\nexport function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult {\n const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? []);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Refs keep `send` stable and always reading the latest values.\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const headersRef = useRef(headers);\n headersRef.current = headers;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const abortRef = useRef<AbortController | null>(null);\n\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setLoading(false);\n }, []);\n\n const reset = useCallback(() => {\n stop();\n setMessages([]);\n setError(null);\n }, [stop]);\n\n const send = useCallback(\n async (message: string) => {\n const content = message.trim();\n if (!content || abortRef.current) return;\n\n // Drop empty turns (e.g. a reply that failed before its first token): backends reject them.\n const history: ChatMessage[] = [...messagesRef.current.filter((m) => m.content.trim()), { role: 'user', content }];\n setMessages(history);\n setLoading(true);\n setError(null);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n // Appends text to the assistant message at `history.length`, creating it on first call.\n const appendToReply = (text: string) => {\n setMessages((prev) => {\n const next = prev.slice();\n const current = next[history.length];\n next[history.length] = { role: 'assistant', content: (current?.content ?? '') + text };\n return next;\n });\n };\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', ...headersRef.current },\n body: JSON.stringify({ messages: history }),\n signal: controller.signal,\n });\n\n if (!res.ok) throw new Error(await readErrorMessage(res));\n\n const contentType = res.headers.get('content-type') ?? '';\n if (contentType.includes('text/event-stream')) {\n if (!res.body) throw new Error('Empty response stream');\n await readSSE(res.body, appendToReply);\n } else {\n const data: unknown = await res.json();\n const reply = data && typeof data === 'object' ? (data as { reply?: unknown }).reply : undefined;\n if (typeof reply !== 'string') throw new Error('Invalid response: expected { reply: string }');\n if (reply) appendToReply(reply);\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n onErrorRef.current?.(e);\n } finally {\n // Skip if `stop()` or a newer request already took over.\n if (abortRef.current === controller) {\n abortRef.current = null;\n setLoading(false);\n }\n }\n },\n [endpoint],\n );\n\n return { messages, loading, error, send, stop, reset };\n}\n\nasync function readErrorMessage(res: Response): Promise<string> {\n try {\n const data: unknown = await res.json();\n const msg = data && typeof data === 'object' ? (data as { error?: unknown }).error : undefined;\n if (typeof msg === 'string' && msg) return msg;\n } catch {\n // not JSON\n }\n return `Request failed with status ${res.status}`;\n}\n\n/** Minimal SSE reader. Accepts `data: {\"delta\": \"...\"}` (also `text`/`content`) or raw text; stops at `[DONE]`. */\nasync function readSSE(body: ReadableStream<Uint8Array>, onDelta: (text: string) => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n const handleEvent = (raw: string): boolean => {\n let event = 'message';\n const dataLines: string[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n if (line.startsWith('event:')) event = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return false;\n const data = dataLines.join('\\n');\n if (data === '[DONE]') return true;\n\n let parsed: unknown = data;\n try {\n parsed = JSON.parse(data);\n } catch {\n // raw text chunk\n }\n\n if (event === 'error') {\n const msg = parsed && typeof parsed === 'object' ? (parsed as { error?: unknown }).error : parsed;\n throw new Error(typeof msg === 'string' && msg ? msg : 'Stream error');\n }\n\n if (typeof parsed === 'string') onDelta(parsed);\n else if (parsed && typeof parsed === 'object') {\n const p = parsed as { delta?: unknown; text?: unknown; content?: unknown };\n const text = p.delta ?? p.text ?? p.content;\n if (typeof text === 'string') onDelta(text);\n }\n return false;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = events.pop() ?? '';\n for (const evt of events) {\n if (handleEvent(evt)) return;\n }\n }\n buffer += decoder.decode();\n if (buffer.trim()) handleEvent(buffer);\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","useState","useRef","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/client/ChatWidget.tsx","../src/client/useChat.ts"],"sourcesContent":["import {\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n} from 'react';\nimport { useChat } from './useChat';\n\n/** `CSSProperties` that also accepts CSS custom properties, e.g. `{ '--cw-accent': '#c8553d' }`. */\nexport type ChatWidgetStyle = CSSProperties & { [key: `--${string}`]: string | number | undefined };\n\nexport type ChatWidgetVariant = 'light' | 'dark';\n\nexport interface ChatWidgetProps {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request. Never put provider API keys here: they would be public. */\n headers?: Record<string, string>;\n /** Input placeholder. */\n placeholder?: string;\n /** Panel title. */\n title?: string;\n /** Message shown when the conversation is empty. Display-only: not sent to the backend. */\n welcomeMessage?: string;\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n /** Open the panel on mount. */\n defaultOpen?: boolean;\n /** Built-in palette used when a `--cw-*` property is not set. */\n variant?: ChatWidgetVariant;\n /** Extra class on the root element. Set `--cw-*` properties on it to theme the widget. */\n className?: string;\n /** Inline styles merged onto the root element. Accepts `--cw-*` properties. */\n style?: ChatWidgetStyle;\n /** Custom launcher icon, shown while the panel is closed. */\n toggleIcon?: ReactNode;\n}\n\nconst FONT = 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n/** Fallback values for every `--cw-*` property. A property set by the consumer always wins. */\nconst LIGHT = {\n font: FONT,\n text: '#111827',\n muted: '#6b7280',\n 'panel-bg': '#ffffff',\n 'list-bg': '#f9fafb',\n border: '#e5e7eb',\n accent: '#2563eb',\n 'accent-contrast': '#ffffff',\n 'bubble-bg': '#ffffff',\n 'bubble-text': '#111827',\n 'input-bg': '#ffffff',\n 'input-border': '#d1d5db',\n 'error-bg': '#fef2f2',\n 'error-text': '#b91c1c',\n shadow: '0 12px 40px rgba(0,0,0,0.18)',\n 'toggle-shadow': '0 6px 20px rgba(0,0,0,0.2)',\n radius: '12px',\n 'radius-sm': '8px',\n};\n\ntype Theme = typeof LIGHT;\n\nconst DARK: Theme = {\n ...LIGHT,\n text: '#e5e0db',\n muted: '#8a8580',\n 'panel-bg': '#0c0c0c',\n 'list-bg': '#0a0a0a',\n border: 'rgba(255,255,255,0.08)',\n 'bubble-bg': 'rgba(255,255,255,0.04)',\n 'bubble-text': '#e5e0db',\n 'input-bg': 'rgba(255,255,255,0.03)',\n 'input-border': 'rgba(255,255,255,0.08)',\n 'error-bg': 'rgba(185,28,28,0.15)',\n 'error-text': '#fca5a5',\n};\n\nfunction createStyles(theme: Theme) {\n const v = (name: keyof Theme) => `var(--cw-${name}, ${theme[name]})`;\n const accent = v('accent');\n const accentContrast = v('accent-contrast');\n const border = `1px solid ${v('border')}`;\n\n return {\n root: {\n position: 'fixed',\n right: 20,\n bottom: 20,\n zIndex: 2147483000,\n fontFamily: v('font'),\n fontSize: 14,\n lineHeight: 1.45,\n color: v('text'),\n boxSizing: 'border-box',\n },\n toggle: {\n width: 56,\n height: 56,\n borderRadius: '50%',\n border: 'none',\n background: accent,\n color: accentContrast,\n cursor: 'pointer',\n boxShadow: v('toggle-shadow'),\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n marginLeft: 'auto',\n padding: 0,\n },\n panel: {\n width: 'min(370px, calc(100vw - 40px))',\n height: 'min(540px, calc(100vh - 110px))',\n marginBottom: 12,\n background: v('panel-bg'),\n borderRadius: v('radius'),\n boxShadow: v('shadow'),\n border,\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n },\n header: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n background: `var(--cw-header-bg, ${accent})`,\n color: `var(--cw-header-text, ${accentContrast})`,\n borderBottom: 'var(--cw-header-border, none)',\n },\n title: { margin: 0, fontSize: 15, fontWeight: 600, fontFamily: 'var(--cw-title-font, inherit)' },\n close: {\n background: 'transparent',\n border: 'none',\n color: 'inherit',\n cursor: 'pointer',\n fontSize: 20,\n lineHeight: 1,\n padding: 4,\n },\n list: {\n flex: 1,\n overflowY: 'auto',\n padding: 14,\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n background: v('list-bg'),\n margin: 0,\n listStyle: 'none',\n },\n bubble: {\n maxWidth: '85%',\n padding: '8px 12px',\n borderRadius: v('radius'),\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n },\n user: { alignSelf: 'flex-end', background: accent, color: accentContrast, borderBottomRightRadius: 4 },\n assistant: {\n alignSelf: 'flex-start',\n background: v('bubble-bg'),\n color: v('bubble-text'),\n border,\n borderBottomLeftRadius: 4,\n },\n status: { alignSelf: 'flex-start', color: v('muted'), fontStyle: 'italic', padding: '0 4px' },\n error: {\n margin: '0 14px 8px',\n padding: '8px 10px',\n borderRadius: v('radius-sm'),\n background: v('error-bg'),\n color: v('error-text'),\n fontSize: 13,\n },\n form: { display: 'flex', gap: 8, padding: 10, borderTop: border, background: v('panel-bg') },\n input: {\n flex: 1,\n resize: 'none',\n border: `1px solid ${v('input-border')}`,\n borderRadius: v('radius-sm'),\n padding: '8px 10px',\n font: 'inherit',\n color: 'inherit',\n background: v('input-bg'),\n minHeight: 38,\n maxHeight: 120,\n boxSizing: 'border-box',\n },\n send: {\n border: 'none',\n borderRadius: v('radius-sm'),\n background: accent,\n color: accentContrast,\n padding: '0 14px',\n font: 'inherit',\n fontWeight: 600,\n cursor: 'pointer',\n },\n srOnly: {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0,0,0,0)',\n whiteSpace: 'nowrap',\n border: 0,\n },\n } satisfies Record<string, CSSProperties>;\n}\n\nconst STYLES: Record<ChatWidgetVariant, ReturnType<typeof createStyles>> = {\n light: createStyles(LIGHT),\n dark: createStyles(DARK),\n};\n\nexport function ChatWidget({\n endpoint,\n headers,\n placeholder = 'Ask me anything…',\n title = 'Chat with me',\n welcomeMessage,\n onError,\n defaultOpen = false,\n variant = 'light',\n className,\n style,\n toggleIcon,\n}: ChatWidgetProps) {\n const styles = STYLES[variant] ?? STYLES.light;\n const [open, setOpen] = useState(defaultOpen);\n const [input, setInput] = useState('');\n const { messages, loading, error, send } = useChat({ endpoint, headers, onError });\n\n const id = useId();\n const panelId = `${id}-panel`;\n const titleId = `${id}-title`;\n const inputId = `${id}-input`;\n\n const toggleRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const listRef = useRef<HTMLOListElement>(null);\n\n useEffect(() => {\n if (open) inputRef.current?.focus();\n }, [open]);\n\n useEffect(() => {\n const el = listRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [messages, loading, open]);\n\n const close = () => {\n setOpen(false);\n toggleRef.current?.focus();\n };\n\n const submit = (e?: FormEvent) => {\n e?.preventDefault();\n if (!input.trim() || loading) return;\n const text = input;\n setInput('');\n void send(text);\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n }\n };\n\n const onPanelKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n close();\n }\n };\n\n const last = messages[messages.length - 1];\n const waitingFirstToken = loading && last?.role !== 'assistant';\n const canSend = input.trim().length > 0 && !loading;\n\n return (\n <div style={{ ...styles.root, ...style }} className={className ? `tr-chat-widget ${className}` : 'tr-chat-widget'}>\n {open && (\n <div\n id={panelId}\n role=\"dialog\"\n aria-labelledby={titleId}\n style={styles.panel}\n className=\"tr-chat-panel\"\n onKeyDown={onPanelKeyDown}\n >\n <div style={styles.header} className=\"tr-chat-header\">\n <h2 id={titleId} style={styles.title}>\n {title}\n </h2>\n <button type=\"button\" onClick={close} aria-label=\"Close chat\" style={styles.close} className=\"tr-chat-close\">\n ×\n </button>\n </div>\n\n <ol ref={listRef} role=\"log\" aria-live=\"polite\" aria-relevant=\"additions text\" aria-busy={loading} style={styles.list} className=\"tr-chat-list\">\n {welcomeMessage && messages.length === 0 && (\n <li style={{ ...styles.bubble, ...styles.assistant }} className=\"tr-chat-msg tr-chat-msg-assistant\">\n {welcomeMessage}\n </li>\n )}\n {messages.map((m, i) => (\n <li\n key={i}\n style={{ ...styles.bubble, ...(m.role === 'user' ? styles.user : styles.assistant) }}\n className={`tr-chat-msg tr-chat-msg-${m.role}`}\n >\n <span style={styles.srOnly}>{m.role === 'user' ? 'You: ' : 'Assistant: '}</span>\n {m.content}\n </li>\n ))}\n {waitingFirstToken && (\n <li style={styles.status} className=\"tr-chat-typing\">\n Typing…\n </li>\n )}\n </ol>\n\n {error && (\n <div role=\"alert\" style={styles.error} className=\"tr-chat-error\">\n {error.message || 'Something went wrong.'}\n </div>\n )}\n\n <form onSubmit={submit} style={styles.form} className=\"tr-chat-form\">\n <label htmlFor={inputId} style={styles.srOnly}>\n Message\n </label>\n <textarea\n id={inputId}\n ref={inputRef}\n rows={1}\n value={input}\n placeholder={placeholder}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={onInputKeyDown}\n style={styles.input}\n className=\"tr-chat-input\"\n />\n <button\n type=\"submit\"\n disabled={!canSend}\n aria-label=\"Send message\"\n style={{ ...styles.send, opacity: canSend ? 1 : 0.5, cursor: canSend ? 'pointer' : 'not-allowed' }}\n className=\"tr-chat-send\"\n >\n Send\n </button>\n </form>\n </div>\n )}\n\n <button\n ref={toggleRef}\n type=\"button\"\n onClick={() => (open ? close() : setOpen(true))}\n aria-label={open ? 'Close chat' : 'Open chat'}\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n style={styles.toggle}\n className=\"tr-chat-toggle\"\n >\n {open ? (\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n ) : toggleIcon !== undefined ? (\n toggleIcon\n ) : (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z\" />\n </svg>\n )}\n </button>\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ChatMessage } from '../types';\n\nexport interface UseChatOptions {\n /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */\n endpoint: string;\n /** Extra headers sent with every request (e.g. a session token). Never put provider API keys here. */\n headers?: Record<string, string>;\n /** Initial conversation history. */\n initialMessages?: ChatMessage[];\n /** Called on network / HTTP / stream errors. */\n onError?: (error: Error) => void;\n}\n\nexport interface UseChatResult {\n messages: ChatMessage[];\n loading: boolean;\n error: Error | null;\n /** Sends a user message and appends the assistant reply. Resolves when the reply is complete. */\n send: (message: string) => Promise<void>;\n /** Aborts the in-flight request, if any. */\n stop: () => void;\n /** Clears the conversation and the error. */\n reset: () => void;\n}\n\nexport function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult {\n const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? []);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Refs keep `send` stable and always reading the latest values.\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const headersRef = useRef(headers);\n headersRef.current = headers;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const abortRef = useRef<AbortController | null>(null);\n\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setLoading(false);\n }, []);\n\n const reset = useCallback(() => {\n stop();\n setMessages([]);\n setError(null);\n }, [stop]);\n\n const send = useCallback(\n async (message: string) => {\n const content = message.trim();\n if (!content || abortRef.current) return;\n\n // Drop empty turns (e.g. a reply that failed before its first token): backends reject them.\n const history: ChatMessage[] = [...messagesRef.current.filter((m) => m.content.trim()), { role: 'user', content }];\n setMessages(history);\n setLoading(true);\n setError(null);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n // Appends text to the assistant message at `history.length`, creating it on first call.\n const appendToReply = (text: string) => {\n setMessages((prev) => {\n const next = prev.slice();\n const current = next[history.length];\n next[history.length] = { role: 'assistant', content: (current?.content ?? '') + text };\n return next;\n });\n };\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', ...headersRef.current },\n body: JSON.stringify({ messages: history }),\n signal: controller.signal,\n });\n\n if (!res.ok) throw new Error(await readErrorMessage(res));\n\n const contentType = res.headers.get('content-type') ?? '';\n if (contentType.includes('text/event-stream')) {\n if (!res.body) throw new Error('Empty response stream');\n await readSSE(res.body, appendToReply);\n } else {\n const data: unknown = await res.json();\n const reply = data && typeof data === 'object' ? (data as { reply?: unknown }).reply : undefined;\n if (typeof reply !== 'string') throw new Error('Invalid response: expected { reply: string }');\n if (reply) appendToReply(reply);\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n onErrorRef.current?.(e);\n } finally {\n // Skip if `stop()` or a newer request already took over.\n if (abortRef.current === controller) {\n abortRef.current = null;\n setLoading(false);\n }\n }\n },\n [endpoint],\n );\n\n return { messages, loading, error, send, stop, reset };\n}\n\nasync function readErrorMessage(res: Response): Promise<string> {\n try {\n const data: unknown = await res.json();\n const msg = data && typeof data === 'object' ? (data as { error?: unknown }).error : undefined;\n if (typeof msg === 'string' && msg) return msg;\n } catch {\n // not JSON\n }\n return `Request failed with status ${res.status}`;\n}\n\n/** Minimal SSE reader. Accepts `data: {\"delta\": \"...\"}` (also `text`/`content`) or raw text; stops at `[DONE]`. */\nasync function readSSE(body: ReadableStream<Uint8Array>, onDelta: (text: string) => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n const handleEvent = (raw: string): boolean => {\n let event = 'message';\n const dataLines: string[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n if (line.startsWith('event:')) event = line.slice(6).trim();\n else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length === 0) return false;\n const data = dataLines.join('\\n');\n if (data === '[DONE]') return true;\n\n let parsed: unknown = data;\n try {\n parsed = JSON.parse(data);\n } catch {\n // raw text chunk\n }\n\n if (event === 'error') {\n const msg = parsed && typeof parsed === 'object' ? (parsed as { error?: unknown }).error : parsed;\n throw new Error(typeof msg === 'string' && msg ? msg : 'Stream error');\n }\n\n if (typeof parsed === 'string') onDelta(parsed);\n else if (parsed && typeof parsed === 'object') {\n const p = parsed as { delta?: unknown; text?: unknown; content?: unknown };\n const text = p.delta ?? p.text ?? p.content;\n if (typeof text === 'string') onDelta(text);\n }\n return false;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = events.pop() ?? '';\n for (const evt of events) {\n if (handleEvent(evt)) return;\n }\n }\n buffer += decoder.decode();\n if (buffer.trim()) handleEvent(buffer);\n } finally {\n reader.cancel().catch(() => {});\n }\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","useState","useRef","useEffect"]}
|
package/dist/styles.css
CHANGED
package/package.json
CHANGED