@trossino/chat-widget 0.0.0-stage → 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.
@@ -0,0 +1,79 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties, ReactNode } from 'react';
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';
9
+ interface ChatWidgetProps {
10
+ /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */
11
+ endpoint: string;
12
+ /** Extra headers sent with every request. Never put provider API keys here: they would be public. */
13
+ headers?: Record<string, string>;
14
+ /** Input placeholder. */
15
+ placeholder?: string;
16
+ /** Panel title. */
17
+ title?: string;
18
+ /** Message shown when the conversation is empty. Display-only: not sent to the backend. */
19
+ welcomeMessage?: string;
20
+ /** Called on network / HTTP / stream errors. */
21
+ onError?: (error: Error) => void;
22
+ /** Open the panel on mount. */
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;
32
+ }
33
+ declare function ChatWidget({ endpoint, headers, placeholder, title, welcomeMessage, onError, defaultOpen, variant, className, style, toggleIcon, }: ChatWidgetProps): react.JSX.Element;
34
+
35
+ type ChatRole = 'user' | 'assistant';
36
+ interface ChatMessage {
37
+ role: ChatRole;
38
+ content: string;
39
+ }
40
+ /** Body sent by the client to the endpoint. */
41
+ interface ChatRequestBody {
42
+ messages: ChatMessage[];
43
+ }
44
+ /** Non-streaming JSON response expected from the endpoint. */
45
+ interface ChatResponseBody {
46
+ reply: string;
47
+ }
48
+ /**
49
+ * One SSE event payload (`data: <json>`) for streaming responses.
50
+ * The stream ends with `data: [DONE]`. Errors are sent as `event: error` + `data: {"error": "..."}`.
51
+ */
52
+ interface ChatStreamChunk {
53
+ delta: string;
54
+ }
55
+
56
+ interface UseChatOptions {
57
+ /** URL that receives `POST { messages }` and answers with `{ reply }` JSON or an SSE stream. */
58
+ endpoint: string;
59
+ /** Extra headers sent with every request (e.g. a session token). Never put provider API keys here. */
60
+ headers?: Record<string, string>;
61
+ /** Initial conversation history. */
62
+ initialMessages?: ChatMessage[];
63
+ /** Called on network / HTTP / stream errors. */
64
+ onError?: (error: Error) => void;
65
+ }
66
+ interface UseChatResult {
67
+ messages: ChatMessage[];
68
+ loading: boolean;
69
+ error: Error | null;
70
+ /** Sends a user message and appends the assistant reply. Resolves when the reply is complete. */
71
+ send: (message: string) => Promise<void>;
72
+ /** Aborts the in-flight request, if any. */
73
+ stop: () => void;
74
+ /** Clears the conversation and the error. */
75
+ reset: () => void;
76
+ }
77
+ declare function useChat({ endpoint, headers, initialMessages, onError }: UseChatOptions): UseChatResult;
78
+
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 ADDED
@@ -0,0 +1,462 @@
1
+ "use client";
2
+
3
+ // src/client/ChatWidget.tsx
4
+ import {
5
+ useEffect as useEffect2,
6
+ useId,
7
+ useRef as useRef2,
8
+ useState as useState2
9
+ } from "react";
10
+
11
+ // src/client/useChat.ts
12
+ import { useCallback, useEffect, useRef, useState } from "react";
13
+ function useChat({ endpoint, headers, initialMessages, onError }) {
14
+ const [messages, setMessages] = useState(initialMessages ?? []);
15
+ const [loading, setLoading] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const messagesRef = useRef(messages);
18
+ messagesRef.current = messages;
19
+ const headersRef = useRef(headers);
20
+ headersRef.current = headers;
21
+ const onErrorRef = useRef(onError);
22
+ onErrorRef.current = onError;
23
+ const abortRef = useRef(null);
24
+ useEffect(() => () => abortRef.current?.abort(), []);
25
+ const stop = useCallback(() => {
26
+ abortRef.current?.abort();
27
+ abortRef.current = null;
28
+ setLoading(false);
29
+ }, []);
30
+ const reset = useCallback(() => {
31
+ stop();
32
+ setMessages([]);
33
+ setError(null);
34
+ }, [stop]);
35
+ const send = useCallback(
36
+ async (message) => {
37
+ const content = message.trim();
38
+ if (!content || abortRef.current) return;
39
+ const history = [...messagesRef.current.filter((m) => m.content.trim()), { role: "user", content }];
40
+ setMessages(history);
41
+ setLoading(true);
42
+ setError(null);
43
+ const controller = new AbortController();
44
+ abortRef.current = controller;
45
+ const appendToReply = (text) => {
46
+ setMessages((prev) => {
47
+ const next = prev.slice();
48
+ const current = next[history.length];
49
+ next[history.length] = { role: "assistant", content: (current?.content ?? "") + text };
50
+ return next;
51
+ });
52
+ };
53
+ try {
54
+ const res = await fetch(endpoint, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...headersRef.current },
57
+ body: JSON.stringify({ messages: history }),
58
+ signal: controller.signal
59
+ });
60
+ if (!res.ok) throw new Error(await readErrorMessage(res));
61
+ const contentType = res.headers.get("content-type") ?? "";
62
+ if (contentType.includes("text/event-stream")) {
63
+ if (!res.body) throw new Error("Empty response stream");
64
+ await readSSE(res.body, appendToReply);
65
+ } else {
66
+ const data = await res.json();
67
+ const reply = data && typeof data === "object" ? data.reply : void 0;
68
+ if (typeof reply !== "string") throw new Error("Invalid response: expected { reply: string }");
69
+ if (reply) appendToReply(reply);
70
+ }
71
+ } catch (err) {
72
+ if (controller.signal.aborted) return;
73
+ const e = err instanceof Error ? err : new Error(String(err));
74
+ setError(e);
75
+ onErrorRef.current?.(e);
76
+ } finally {
77
+ if (abortRef.current === controller) {
78
+ abortRef.current = null;
79
+ setLoading(false);
80
+ }
81
+ }
82
+ },
83
+ [endpoint]
84
+ );
85
+ return { messages, loading, error, send, stop, reset };
86
+ }
87
+ async function readErrorMessage(res) {
88
+ try {
89
+ const data = await res.json();
90
+ const msg = data && typeof data === "object" ? data.error : void 0;
91
+ if (typeof msg === "string" && msg) return msg;
92
+ } catch {
93
+ }
94
+ return `Request failed with status ${res.status}`;
95
+ }
96
+ async function readSSE(body, onDelta) {
97
+ const reader = body.getReader();
98
+ const decoder = new TextDecoder();
99
+ let buffer = "";
100
+ const handleEvent = (raw) => {
101
+ let event = "message";
102
+ const dataLines = [];
103
+ for (const line of raw.split(/\r?\n/)) {
104
+ if (line.startsWith("event:")) event = line.slice(6).trim();
105
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
106
+ }
107
+ if (dataLines.length === 0) return false;
108
+ const data = dataLines.join("\n");
109
+ if (data === "[DONE]") return true;
110
+ let parsed = data;
111
+ try {
112
+ parsed = JSON.parse(data);
113
+ } catch {
114
+ }
115
+ if (event === "error") {
116
+ const msg = parsed && typeof parsed === "object" ? parsed.error : parsed;
117
+ throw new Error(typeof msg === "string" && msg ? msg : "Stream error");
118
+ }
119
+ if (typeof parsed === "string") onDelta(parsed);
120
+ else if (parsed && typeof parsed === "object") {
121
+ const p = parsed;
122
+ const text = p.delta ?? p.text ?? p.content;
123
+ if (typeof text === "string") onDelta(text);
124
+ }
125
+ return false;
126
+ };
127
+ try {
128
+ while (true) {
129
+ const { done, value } = await reader.read();
130
+ if (done) break;
131
+ buffer += decoder.decode(value, { stream: true });
132
+ const events = buffer.split(/\r?\n\r?\n/);
133
+ buffer = events.pop() ?? "";
134
+ for (const evt of events) {
135
+ if (handleEvent(evt)) return;
136
+ }
137
+ }
138
+ buffer += decoder.decode();
139
+ if (buffer.trim()) handleEvent(buffer);
140
+ } finally {
141
+ reader.cancel().catch(() => {
142
+ });
143
+ }
144
+ }
145
+
146
+ // src/client/ChatWidget.tsx
147
+ import { jsx, jsxs } from "react/jsx-runtime";
148
+ var FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
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)
321
+ };
322
+ function ChatWidget({
323
+ endpoint,
324
+ headers,
325
+ placeholder = "Ask me anything\u2026",
326
+ title = "Chat with me",
327
+ welcomeMessage,
328
+ onError,
329
+ defaultOpen = false,
330
+ variant = "light",
331
+ className,
332
+ style,
333
+ toggleIcon
334
+ }) {
335
+ const styles = STYLES[variant] ?? STYLES.light;
336
+ const [open, setOpen] = useState2(defaultOpen);
337
+ const [input, setInput] = useState2("");
338
+ const { messages, loading, error, send } = useChat({ endpoint, headers, onError });
339
+ const id = useId();
340
+ const panelId = `${id}-panel`;
341
+ const titleId = `${id}-title`;
342
+ const inputId = `${id}-input`;
343
+ const toggleRef = useRef2(null);
344
+ const inputRef = useRef2(null);
345
+ const listRef = useRef2(null);
346
+ useEffect2(() => {
347
+ if (open) inputRef.current?.focus();
348
+ }, [open]);
349
+ useEffect2(() => {
350
+ const el = listRef.current;
351
+ if (el) el.scrollTop = el.scrollHeight;
352
+ }, [messages, loading, open]);
353
+ const close = () => {
354
+ setOpen(false);
355
+ toggleRef.current?.focus();
356
+ };
357
+ const submit = (e) => {
358
+ e?.preventDefault();
359
+ if (!input.trim() || loading) return;
360
+ const text = input;
361
+ setInput("");
362
+ void send(text);
363
+ };
364
+ const onInputKeyDown = (e) => {
365
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
366
+ e.preventDefault();
367
+ submit();
368
+ }
369
+ };
370
+ const onPanelKeyDown = (e) => {
371
+ if (e.key === "Escape") {
372
+ e.stopPropagation();
373
+ close();
374
+ }
375
+ };
376
+ const last = messages[messages.length - 1];
377
+ const waitingFirstToken = loading && last?.role !== "assistant";
378
+ const canSend = input.trim().length > 0 && !loading;
379
+ return /* @__PURE__ */ jsxs("div", { style: { ...styles.root, ...style }, className: className ? `tr-chat-widget ${className}` : "tr-chat-widget", children: [
380
+ open && /* @__PURE__ */ jsxs(
381
+ "div",
382
+ {
383
+ id: panelId,
384
+ role: "dialog",
385
+ "aria-labelledby": titleId,
386
+ style: styles.panel,
387
+ className: "tr-chat-panel",
388
+ onKeyDown: onPanelKeyDown,
389
+ children: [
390
+ /* @__PURE__ */ jsxs("div", { style: styles.header, className: "tr-chat-header", children: [
391
+ /* @__PURE__ */ jsx("h2", { id: titleId, style: styles.title, children: title }),
392
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: close, "aria-label": "Close chat", style: styles.close, className: "tr-chat-close", children: "\xD7" })
393
+ ] }),
394
+ /* @__PURE__ */ jsxs("ol", { ref: listRef, role: "log", "aria-live": "polite", "aria-relevant": "additions text", "aria-busy": loading, style: styles.list, className: "tr-chat-list", children: [
395
+ welcomeMessage && messages.length === 0 && /* @__PURE__ */ jsx("li", { style: { ...styles.bubble, ...styles.assistant }, className: "tr-chat-msg tr-chat-msg-assistant", children: welcomeMessage }),
396
+ messages.map((m, i) => /* @__PURE__ */ jsxs(
397
+ "li",
398
+ {
399
+ style: { ...styles.bubble, ...m.role === "user" ? styles.user : styles.assistant },
400
+ className: `tr-chat-msg tr-chat-msg-${m.role}`,
401
+ children: [
402
+ /* @__PURE__ */ jsx("span", { style: styles.srOnly, children: m.role === "user" ? "You: " : "Assistant: " }),
403
+ m.content
404
+ ]
405
+ },
406
+ i
407
+ )),
408
+ waitingFirstToken && /* @__PURE__ */ jsx("li", { style: styles.status, className: "tr-chat-typing", children: "Typing\u2026" })
409
+ ] }),
410
+ error && /* @__PURE__ */ jsx("div", { role: "alert", style: styles.error, className: "tr-chat-error", children: error.message || "Something went wrong." }),
411
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, style: styles.form, className: "tr-chat-form", children: [
412
+ /* @__PURE__ */ jsx("label", { htmlFor: inputId, style: styles.srOnly, children: "Message" }),
413
+ /* @__PURE__ */ jsx(
414
+ "textarea",
415
+ {
416
+ id: inputId,
417
+ ref: inputRef,
418
+ rows: 1,
419
+ value: input,
420
+ placeholder,
421
+ onChange: (e) => setInput(e.target.value),
422
+ onKeyDown: onInputKeyDown,
423
+ style: styles.input,
424
+ className: "tr-chat-input"
425
+ }
426
+ ),
427
+ /* @__PURE__ */ jsx(
428
+ "button",
429
+ {
430
+ type: "submit",
431
+ disabled: !canSend,
432
+ "aria-label": "Send message",
433
+ style: { ...styles.send, opacity: canSend ? 1 : 0.5, cursor: canSend ? "pointer" : "not-allowed" },
434
+ className: "tr-chat-send",
435
+ children: "Send"
436
+ }
437
+ )
438
+ ] })
439
+ ]
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx(
443
+ "button",
444
+ {
445
+ ref: toggleRef,
446
+ type: "button",
447
+ onClick: () => open ? close() : setOpen(true),
448
+ "aria-label": open ? "Close chat" : "Open chat",
449
+ "aria-expanded": open,
450
+ "aria-controls": open ? panelId : void 0,
451
+ style: styles.toggle,
452
+ className: "tr-chat-toggle",
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" }) })
454
+ }
455
+ )
456
+ ] });
457
+ }
458
+ export {
459
+ ChatWidget,
460
+ useChat
461
+ };
462
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
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"]}