@trossino/chat-widget 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/dist/index.cjs +429 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +402 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +249 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +45 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.js +246 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +29 -0
- package/package.json +90 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tommaso Rossino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,117 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @trossino/chat-widget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Batteries-included React chat widget, plus an optional framework-agnostic server handler for Anthropic and OpenAI.
|
|
4
|
+
|
|
5
|
+
- Drop-in UI: floating bottom-right panel, fully styled inline (no Tailwind, no CSS file required).
|
|
6
|
+
- Zero runtime dependencies: native `fetch`, Web `Request`/`Response`.
|
|
7
|
+
- JSON or SSE streaming responses, detected automatically from `Content-Type`.
|
|
8
|
+
- Works with Next.js App Router, Vercel, Cloudflare Workers, Deno, Bun, or any Web-standard runtime.
|
|
9
|
+
- Accessible: labelled controls, `role="log"` live region, Enter to send, Shift+Enter for newline, Esc to close.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @trossino/chat-widget
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependencies: `react` and `react-dom` 18 or 19.
|
|
18
|
+
|
|
19
|
+
## Client only (your own endpoint)
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { ChatWidget } from '@trossino/chat-widget';
|
|
23
|
+
|
|
24
|
+
export default function App() {
|
|
25
|
+
return <ChatWidget endpoint="/api/my-chat" title="Support" welcomeMessage="Hi! How can I help?" />;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Your endpoint receives `POST { messages: Array<{ role: 'user' | 'assistant', content: string }> }` and must answer with either:
|
|
30
|
+
|
|
31
|
+
- JSON: `{ "reply": "..." }`
|
|
32
|
+
- SSE (`Content-Type: text/event-stream`): `data: {"delta":"..."}` events, ending with `data: [DONE]`. Errors: `event: error` + `data: {"error":"..."}`.
|
|
33
|
+
|
|
34
|
+
Want your own UI? Use the hook directly:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useChat } from '@trossino/chat-widget';
|
|
38
|
+
|
|
39
|
+
const { messages, loading, error, send, stop, reset } = useChat({ endpoint: '/api/chat' });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Batteries included (Next.js App Router)
|
|
43
|
+
|
|
44
|
+
`app/api/chat/route.ts`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createChatHandler } from '@trossino/chat-widget/server';
|
|
48
|
+
|
|
49
|
+
export const POST = createChatHandler({
|
|
50
|
+
provider: 'anthropic', // or 'openai'
|
|
51
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? '',
|
|
52
|
+
systemPrompt: 'You are a concise, friendly assistant.',
|
|
53
|
+
maxTokens: 1024,
|
|
54
|
+
stream: true,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`app/page.tsx`:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { ChatWidget } from '@trossino/chat-widget';
|
|
62
|
+
|
|
63
|
+
export default function Page() {
|
|
64
|
+
return <ChatWidget endpoint="/api/chat" title="Ask me anything" />;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The client bundle ships with `"use client"`, so it can be rendered from Server Components. See [`examples/nextjs`](./examples/nextjs) for a full app.
|
|
69
|
+
|
|
70
|
+
The handler is a plain `(req: Request) => Promise<Response>`, so it also works as a Cloudflare Worker (`export default { fetch: handler }`) or with `Deno.serve(handler)`.
|
|
71
|
+
|
|
72
|
+
## `<ChatWidget>` props
|
|
73
|
+
|
|
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` | `"Type a message…"` | Input placeholder. |
|
|
79
|
+
| `title` | `string` | `"Chat"` | 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
|
+
|
|
84
|
+
## `createChatHandler` options
|
|
85
|
+
|
|
86
|
+
| Option | Type | Default |
|
|
87
|
+
| ------------------ | ------------------------- | ------------------------------------------------------- |
|
|
88
|
+
| `provider` | `'anthropic' \| 'openai'` | — (required) |
|
|
89
|
+
| `apiKey` | `string` | — (required; responds `401` if empty) |
|
|
90
|
+
| `model` | `string` | `claude-sonnet-4-5` (anthropic), `gpt-4o-mini` (openai) |
|
|
91
|
+
| `systemPrompt` | `string` | — |
|
|
92
|
+
| `maxTokens` | `number` | `1024` |
|
|
93
|
+
| `stream` | `boolean` | `false` (`true` responds with SSE) |
|
|
94
|
+
| `maxMessages` | `number` | `50` |
|
|
95
|
+
| `maxMessageLength` | `number` | `8000` characters |
|
|
96
|
+
| `onError` | `(error: unknown) => void`| — (server-side logging hook) |
|
|
97
|
+
|
|
98
|
+
Responses: `200` with `{ reply }` or an SSE stream, `400` for an invalid body, `401` if the API key is missing, `405` for non-POST, `500` with a generic message for any upstream or internal failure. Provider error details are passed to `onError` and never sent to the client.
|
|
99
|
+
|
|
100
|
+
## Optional styles
|
|
101
|
+
|
|
102
|
+
Everything is inline-styled. For extras inline styles can't express (focus rings, hover, entry animation that respects `prefers-reduced-motion`):
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import '@trossino/chat-widget/styles';
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Notes
|
|
109
|
+
|
|
110
|
+
- **Streaming:** with `stream: true` the handler converts the provider's stream into the simple `delta` protocol above. The client renders tokens as they arrive and also accepts plain JSON, so you can switch freely.
|
|
111
|
+
- **Rate limiting is up to you.** The handler has no auth or rate limiting. Anyone who can reach your endpoint spends your tokens. Put it behind your own auth, rate limiter, or edge middleware (e.g. Upstash, Cloudflare rate limiting rules).
|
|
112
|
+
- **API keys:** the widget never handles provider keys. Keep them in server-side env vars. Anything passed via `headers` is visible in the browser.
|
|
113
|
+
- **Privacy:** messages go from the browser to your endpoint, then to the provider you configure. Nothing is stored or logged by this package. Disclose the provider to your users as your privacy policy requires.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
ChatWidget: () => ChatWidget,
|
|
25
|
+
useChat: () => useChat
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
|
|
29
|
+
// src/client/ChatWidget.tsx
|
|
30
|
+
var import_react2 = require("react");
|
|
31
|
+
|
|
32
|
+
// src/client/useChat.ts
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
function useChat({ endpoint, headers, initialMessages, onError }) {
|
|
35
|
+
const [messages, setMessages] = (0, import_react.useState)(initialMessages ?? []);
|
|
36
|
+
const [loading, setLoading] = (0, import_react.useState)(false);
|
|
37
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
38
|
+
const messagesRef = (0, import_react.useRef)(messages);
|
|
39
|
+
messagesRef.current = messages;
|
|
40
|
+
const headersRef = (0, import_react.useRef)(headers);
|
|
41
|
+
headersRef.current = headers;
|
|
42
|
+
const onErrorRef = (0, import_react.useRef)(onError);
|
|
43
|
+
onErrorRef.current = onError;
|
|
44
|
+
const abortRef = (0, import_react.useRef)(null);
|
|
45
|
+
(0, import_react.useEffect)(() => () => abortRef.current?.abort(), []);
|
|
46
|
+
const stop = (0, import_react.useCallback)(() => {
|
|
47
|
+
abortRef.current?.abort();
|
|
48
|
+
abortRef.current = null;
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}, []);
|
|
51
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
52
|
+
stop();
|
|
53
|
+
setMessages([]);
|
|
54
|
+
setError(null);
|
|
55
|
+
}, [stop]);
|
|
56
|
+
const send = (0, import_react.useCallback)(
|
|
57
|
+
async (message) => {
|
|
58
|
+
const content = message.trim();
|
|
59
|
+
if (!content || abortRef.current) return;
|
|
60
|
+
const history = [...messagesRef.current.filter((m) => m.content.trim()), { role: "user", content }];
|
|
61
|
+
setMessages(history);
|
|
62
|
+
setLoading(true);
|
|
63
|
+
setError(null);
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
abortRef.current = controller;
|
|
66
|
+
const appendToReply = (text) => {
|
|
67
|
+
setMessages((prev) => {
|
|
68
|
+
const next = prev.slice();
|
|
69
|
+
const current = next[history.length];
|
|
70
|
+
next[history.length] = { role: "assistant", content: (current?.content ?? "") + text };
|
|
71
|
+
return next;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(endpoint, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", ...headersRef.current },
|
|
78
|
+
body: JSON.stringify({ messages: history }),
|
|
79
|
+
signal: controller.signal
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) throw new Error(await readErrorMessage(res));
|
|
82
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
83
|
+
if (contentType.includes("text/event-stream")) {
|
|
84
|
+
if (!res.body) throw new Error("Empty response stream");
|
|
85
|
+
await readSSE(res.body, appendToReply);
|
|
86
|
+
} else {
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
const reply = data && typeof data === "object" ? data.reply : void 0;
|
|
89
|
+
if (typeof reply !== "string") throw new Error("Invalid response: expected { reply: string }");
|
|
90
|
+
if (reply) appendToReply(reply);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (controller.signal.aborted) return;
|
|
94
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
95
|
+
setError(e);
|
|
96
|
+
onErrorRef.current?.(e);
|
|
97
|
+
} finally {
|
|
98
|
+
if (abortRef.current === controller) {
|
|
99
|
+
abortRef.current = null;
|
|
100
|
+
setLoading(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[endpoint]
|
|
105
|
+
);
|
|
106
|
+
return { messages, loading, error, send, stop, reset };
|
|
107
|
+
}
|
|
108
|
+
async function readErrorMessage(res) {
|
|
109
|
+
try {
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
const msg = data && typeof data === "object" ? data.error : void 0;
|
|
112
|
+
if (typeof msg === "string" && msg) return msg;
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
return `Request failed with status ${res.status}`;
|
|
116
|
+
}
|
|
117
|
+
async function readSSE(body, onDelta) {
|
|
118
|
+
const reader = body.getReader();
|
|
119
|
+
const decoder = new TextDecoder();
|
|
120
|
+
let buffer = "";
|
|
121
|
+
const handleEvent = (raw) => {
|
|
122
|
+
let event = "message";
|
|
123
|
+
const dataLines = [];
|
|
124
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
125
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
126
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
127
|
+
}
|
|
128
|
+
if (dataLines.length === 0) return false;
|
|
129
|
+
const data = dataLines.join("\n");
|
|
130
|
+
if (data === "[DONE]") return true;
|
|
131
|
+
let parsed = data;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(data);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
if (event === "error") {
|
|
137
|
+
const msg = parsed && typeof parsed === "object" ? parsed.error : parsed;
|
|
138
|
+
throw new Error(typeof msg === "string" && msg ? msg : "Stream error");
|
|
139
|
+
}
|
|
140
|
+
if (typeof parsed === "string") onDelta(parsed);
|
|
141
|
+
else if (parsed && typeof parsed === "object") {
|
|
142
|
+
const p = parsed;
|
|
143
|
+
const text = p.delta ?? p.text ?? p.content;
|
|
144
|
+
if (typeof text === "string") onDelta(text);
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
while (true) {
|
|
150
|
+
const { done, value } = await reader.read();
|
|
151
|
+
if (done) break;
|
|
152
|
+
buffer += decoder.decode(value, { stream: true });
|
|
153
|
+
const events = buffer.split(/\r?\n\r?\n/);
|
|
154
|
+
buffer = events.pop() ?? "";
|
|
155
|
+
for (const evt of events) {
|
|
156
|
+
if (handleEvent(evt)) return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
buffer += decoder.decode();
|
|
160
|
+
if (buffer.trim()) handleEvent(buffer);
|
|
161
|
+
} finally {
|
|
162
|
+
reader.cancel().catch(() => {
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/client/ChatWidget.tsx
|
|
168
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
169
|
+
var ACCENT = "#2563eb";
|
|
170
|
+
var FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
171
|
+
var styles = {
|
|
172
|
+
root: {
|
|
173
|
+
position: "fixed",
|
|
174
|
+
right: 20,
|
|
175
|
+
bottom: 20,
|
|
176
|
+
zIndex: 2147483e3,
|
|
177
|
+
fontFamily: FONT,
|
|
178
|
+
fontSize: 14,
|
|
179
|
+
lineHeight: 1.45,
|
|
180
|
+
color: "#111827",
|
|
181
|
+
boxSizing: "border-box"
|
|
182
|
+
},
|
|
183
|
+
toggle: {
|
|
184
|
+
width: 56,
|
|
185
|
+
height: 56,
|
|
186
|
+
borderRadius: "50%",
|
|
187
|
+
border: "none",
|
|
188
|
+
background: ACCENT,
|
|
189
|
+
color: "#fff",
|
|
190
|
+
cursor: "pointer",
|
|
191
|
+
boxShadow: "0 6px 20px rgba(0,0,0,0.2)",
|
|
192
|
+
display: "flex",
|
|
193
|
+
alignItems: "center",
|
|
194
|
+
justifyContent: "center",
|
|
195
|
+
marginLeft: "auto",
|
|
196
|
+
padding: 0
|
|
197
|
+
},
|
|
198
|
+
panel: {
|
|
199
|
+
width: "min(370px, calc(100vw - 40px))",
|
|
200
|
+
height: "min(540px, calc(100vh - 110px))",
|
|
201
|
+
marginBottom: 12,
|
|
202
|
+
background: "#fff",
|
|
203
|
+
borderRadius: 12,
|
|
204
|
+
boxShadow: "0 12px 40px rgba(0,0,0,0.18)",
|
|
205
|
+
border: "1px solid #e5e7eb",
|
|
206
|
+
display: "flex",
|
|
207
|
+
flexDirection: "column",
|
|
208
|
+
overflow: "hidden"
|
|
209
|
+
},
|
|
210
|
+
header: {
|
|
211
|
+
display: "flex",
|
|
212
|
+
alignItems: "center",
|
|
213
|
+
justifyContent: "space-between",
|
|
214
|
+
padding: "12px 14px",
|
|
215
|
+
background: ACCENT,
|
|
216
|
+
color: "#fff"
|
|
217
|
+
},
|
|
218
|
+
title: { margin: 0, fontSize: 15, fontWeight: 600 },
|
|
219
|
+
close: {
|
|
220
|
+
background: "transparent",
|
|
221
|
+
border: "none",
|
|
222
|
+
color: "#fff",
|
|
223
|
+
cursor: "pointer",
|
|
224
|
+
fontSize: 20,
|
|
225
|
+
lineHeight: 1,
|
|
226
|
+
padding: 4
|
|
227
|
+
},
|
|
228
|
+
list: {
|
|
229
|
+
flex: 1,
|
|
230
|
+
overflowY: "auto",
|
|
231
|
+
padding: 14,
|
|
232
|
+
display: "flex",
|
|
233
|
+
flexDirection: "column",
|
|
234
|
+
gap: 8,
|
|
235
|
+
background: "#f9fafb",
|
|
236
|
+
margin: 0,
|
|
237
|
+
listStyle: "none"
|
|
238
|
+
},
|
|
239
|
+
bubble: {
|
|
240
|
+
maxWidth: "85%",
|
|
241
|
+
padding: "8px 12px",
|
|
242
|
+
borderRadius: 12,
|
|
243
|
+
whiteSpace: "pre-wrap",
|
|
244
|
+
wordBreak: "break-word"
|
|
245
|
+
},
|
|
246
|
+
user: { alignSelf: "flex-end", background: ACCENT, color: "#fff", borderBottomRightRadius: 4 },
|
|
247
|
+
assistant: { alignSelf: "flex-start", background: "#fff", border: "1px solid #e5e7eb", borderBottomLeftRadius: 4 },
|
|
248
|
+
status: { alignSelf: "flex-start", color: "#6b7280", fontStyle: "italic", padding: "0 4px" },
|
|
249
|
+
error: {
|
|
250
|
+
margin: "0 14px 8px",
|
|
251
|
+
padding: "8px 10px",
|
|
252
|
+
borderRadius: 8,
|
|
253
|
+
background: "#fef2f2",
|
|
254
|
+
color: "#b91c1c",
|
|
255
|
+
fontSize: 13
|
|
256
|
+
},
|
|
257
|
+
form: { display: "flex", gap: 8, padding: 10, borderTop: "1px solid #e5e7eb", background: "#fff" },
|
|
258
|
+
input: {
|
|
259
|
+
flex: 1,
|
|
260
|
+
resize: "none",
|
|
261
|
+
border: "1px solid #d1d5db",
|
|
262
|
+
borderRadius: 8,
|
|
263
|
+
padding: "8px 10px",
|
|
264
|
+
font: "inherit",
|
|
265
|
+
color: "inherit",
|
|
266
|
+
background: "#fff",
|
|
267
|
+
minHeight: 38,
|
|
268
|
+
maxHeight: 120,
|
|
269
|
+
boxSizing: "border-box"
|
|
270
|
+
},
|
|
271
|
+
send: {
|
|
272
|
+
border: "none",
|
|
273
|
+
borderRadius: 8,
|
|
274
|
+
background: ACCENT,
|
|
275
|
+
color: "#fff",
|
|
276
|
+
padding: "0 14px",
|
|
277
|
+
font: "inherit",
|
|
278
|
+
fontWeight: 600,
|
|
279
|
+
cursor: "pointer"
|
|
280
|
+
},
|
|
281
|
+
srOnly: {
|
|
282
|
+
position: "absolute",
|
|
283
|
+
width: 1,
|
|
284
|
+
height: 1,
|
|
285
|
+
padding: 0,
|
|
286
|
+
margin: -1,
|
|
287
|
+
overflow: "hidden",
|
|
288
|
+
clip: "rect(0,0,0,0)",
|
|
289
|
+
whiteSpace: "nowrap",
|
|
290
|
+
border: 0
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
function ChatWidget({
|
|
294
|
+
endpoint,
|
|
295
|
+
headers,
|
|
296
|
+
placeholder = "Type a message\u2026",
|
|
297
|
+
title = "Chat",
|
|
298
|
+
welcomeMessage,
|
|
299
|
+
onError,
|
|
300
|
+
defaultOpen = false
|
|
301
|
+
}) {
|
|
302
|
+
const [open, setOpen] = (0, import_react2.useState)(defaultOpen);
|
|
303
|
+
const [input, setInput] = (0, import_react2.useState)("");
|
|
304
|
+
const { messages, loading, error, send } = useChat({ endpoint, headers, onError });
|
|
305
|
+
const id = (0, import_react2.useId)();
|
|
306
|
+
const panelId = `${id}-panel`;
|
|
307
|
+
const titleId = `${id}-title`;
|
|
308
|
+
const inputId = `${id}-input`;
|
|
309
|
+
const toggleRef = (0, import_react2.useRef)(null);
|
|
310
|
+
const inputRef = (0, import_react2.useRef)(null);
|
|
311
|
+
const listRef = (0, import_react2.useRef)(null);
|
|
312
|
+
(0, import_react2.useEffect)(() => {
|
|
313
|
+
if (open) inputRef.current?.focus();
|
|
314
|
+
}, [open]);
|
|
315
|
+
(0, import_react2.useEffect)(() => {
|
|
316
|
+
const el = listRef.current;
|
|
317
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
318
|
+
}, [messages, loading, open]);
|
|
319
|
+
const close = () => {
|
|
320
|
+
setOpen(false);
|
|
321
|
+
toggleRef.current?.focus();
|
|
322
|
+
};
|
|
323
|
+
const submit = (e) => {
|
|
324
|
+
e?.preventDefault();
|
|
325
|
+
if (!input.trim() || loading) return;
|
|
326
|
+
const text = input;
|
|
327
|
+
setInput("");
|
|
328
|
+
void send(text);
|
|
329
|
+
};
|
|
330
|
+
const onInputKeyDown = (e) => {
|
|
331
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
submit();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const onPanelKeyDown = (e) => {
|
|
337
|
+
if (e.key === "Escape") {
|
|
338
|
+
e.stopPropagation();
|
|
339
|
+
close();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const last = messages[messages.length - 1];
|
|
343
|
+
const waitingFirstToken = loading && last?.role !== "assistant";
|
|
344
|
+
const canSend = input.trim().length > 0 && !loading;
|
|
345
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.root, className: "tr-chat-widget", children: [
|
|
346
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
347
|
+
"div",
|
|
348
|
+
{
|
|
349
|
+
id: panelId,
|
|
350
|
+
role: "dialog",
|
|
351
|
+
"aria-labelledby": titleId,
|
|
352
|
+
style: styles.panel,
|
|
353
|
+
className: "tr-chat-panel",
|
|
354
|
+
onKeyDown: onPanelKeyDown,
|
|
355
|
+
children: [
|
|
356
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.header, className: "tr-chat-header", children: [
|
|
357
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { id: titleId, style: styles.title, children: title }),
|
|
358
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: close, "aria-label": "Close chat", style: styles.close, className: "tr-chat-close", children: "\xD7" })
|
|
359
|
+
] }),
|
|
360
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ol", { ref: listRef, role: "log", "aria-live": "polite", "aria-relevant": "additions text", "aria-busy": loading, style: styles.list, className: "tr-chat-list", children: [
|
|
361
|
+
welcomeMessage && messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { ...styles.bubble, ...styles.assistant }, className: "tr-chat-msg tr-chat-msg-assistant", children: welcomeMessage }),
|
|
362
|
+
messages.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
363
|
+
"li",
|
|
364
|
+
{
|
|
365
|
+
style: { ...styles.bubble, ...m.role === "user" ? styles.user : styles.assistant },
|
|
366
|
+
className: `tr-chat-msg tr-chat-msg-${m.role}`,
|
|
367
|
+
children: [
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.srOnly, children: m.role === "user" ? "You: " : "Assistant: " }),
|
|
369
|
+
m.content
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
i
|
|
373
|
+
)),
|
|
374
|
+
waitingFirstToken && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: styles.status, className: "tr-chat-typing", children: "Typing\u2026" })
|
|
375
|
+
] }),
|
|
376
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: styles.error, className: "tr-chat-error", children: error.message || "Something went wrong." }),
|
|
377
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, style: styles.form, className: "tr-chat-form", children: [
|
|
378
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: inputId, style: styles.srOnly, children: "Message" }),
|
|
379
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
380
|
+
"textarea",
|
|
381
|
+
{
|
|
382
|
+
id: inputId,
|
|
383
|
+
ref: inputRef,
|
|
384
|
+
rows: 1,
|
|
385
|
+
value: input,
|
|
386
|
+
placeholder,
|
|
387
|
+
onChange: (e) => setInput(e.target.value),
|
|
388
|
+
onKeyDown: onInputKeyDown,
|
|
389
|
+
style: styles.input,
|
|
390
|
+
className: "tr-chat-input"
|
|
391
|
+
}
|
|
392
|
+
),
|
|
393
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
394
|
+
"button",
|
|
395
|
+
{
|
|
396
|
+
type: "submit",
|
|
397
|
+
disabled: !canSend,
|
|
398
|
+
"aria-label": "Send message",
|
|
399
|
+
style: { ...styles.send, opacity: canSend ? 1 : 0.5, cursor: canSend ? "pointer" : "not-allowed" },
|
|
400
|
+
className: "tr-chat-send",
|
|
401
|
+
children: "Send"
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
] })
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
),
|
|
408
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
409
|
+
"button",
|
|
410
|
+
{
|
|
411
|
+
ref: toggleRef,
|
|
412
|
+
type: "button",
|
|
413
|
+
onClick: () => open ? close() : setOpen(true),
|
|
414
|
+
"aria-label": open ? "Close chat" : "Open chat",
|
|
415
|
+
"aria-expanded": open,
|
|
416
|
+
"aria-controls": open ? panelId : void 0,
|
|
417
|
+
style: styles.toggle,
|
|
418
|
+
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" }) })
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
] });
|
|
423
|
+
}
|
|
424
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
425
|
+
0 && (module.exports = {
|
|
426
|
+
ChatWidget,
|
|
427
|
+
useChat
|
|
428
|
+
});
|
|
429
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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"]}
|