@deltakit/react 0.1.0 → 0.1.2
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 +144 -0
- package/dist/index.cjs +93 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +94 -14
- package/dist/index.js.map +1 -1
- package/package.json +48 -48
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @deltakit/react
|
|
2
|
+
|
|
3
|
+
React hook for building streaming chat UIs over Server-Sent Events (SSE). Manages the entire lifecycle -- state, network requests, SSE parsing, cancellation, and event handling -- in a single `useStreamChat` hook.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @deltakit/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires React 18+ and a backend endpoint that streams SSE.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useStreamChat } from "@deltakit/react";
|
|
17
|
+
|
|
18
|
+
function Chat() {
|
|
19
|
+
const { messages, isLoading, sendMessage } = useStreamChat({
|
|
20
|
+
api: "/api/chat",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
{messages.map((msg) => (
|
|
26
|
+
<div key={msg.id}>
|
|
27
|
+
<strong>{msg.role}:</strong>{" "}
|
|
28
|
+
{msg.parts
|
|
29
|
+
.filter((p) => p.type === "text")
|
|
30
|
+
.map((p) => p.text)
|
|
31
|
+
.join("")}
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
|
|
35
|
+
{isLoading && <span>Thinking...</span>}
|
|
36
|
+
|
|
37
|
+
<form
|
|
38
|
+
onSubmit={(e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
const input = e.currentTarget.elements.namedItem("message") as HTMLInputElement;
|
|
41
|
+
sendMessage(input.value);
|
|
42
|
+
input.value = "";
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<input name="message" placeholder="Type a message..." />
|
|
46
|
+
<button type="submit" disabled={isLoading}>Send</button>
|
|
47
|
+
</form>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
### `useStreamChat(options)`
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const {
|
|
59
|
+
messages, // Message[] -- live-updating conversation
|
|
60
|
+
isLoading, // boolean -- true while streaming
|
|
61
|
+
error, // Error | null -- latest error
|
|
62
|
+
sendMessage, // (text: string) => void -- send and start streaming
|
|
63
|
+
stop, // () => void -- abort current stream
|
|
64
|
+
setMessages, // React setState -- direct state control
|
|
65
|
+
} = useStreamChat({
|
|
66
|
+
api: "/api/chat", // Required. SSE endpoint URL
|
|
67
|
+
initialMessages: [], // Pre-populate conversation (e.g. from DB)
|
|
68
|
+
headers: {}, // Extra fetch headers (e.g. Authorization)
|
|
69
|
+
body: {}, // Extra POST body fields
|
|
70
|
+
onEvent: (event, helpers) => {}, // Custom event handler (replaces default)
|
|
71
|
+
onFinish: (messages) => {}, // Stream ended
|
|
72
|
+
onMessage: (message) => {}, // New message added
|
|
73
|
+
onError: (error) => {}, // Fetch/stream error
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Event Helpers
|
|
78
|
+
|
|
79
|
+
When using `onEvent`, you receive helpers for mutating message state during streaming:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
onEvent: (event, { appendText, appendPart, setMessages }) => {
|
|
83
|
+
switch (event.type) {
|
|
84
|
+
case "text_delta":
|
|
85
|
+
appendText(event.delta);
|
|
86
|
+
break;
|
|
87
|
+
case "tool_call":
|
|
88
|
+
appendPart({
|
|
89
|
+
type: "tool_call",
|
|
90
|
+
tool_name: event.tool_name,
|
|
91
|
+
argument: event.argument,
|
|
92
|
+
callId: event.call_id,
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
case "tool_result":
|
|
96
|
+
// Use setMessages for complex mutations
|
|
97
|
+
setMessages((prev) =>
|
|
98
|
+
prev.map((msg) => ({
|
|
99
|
+
...msg,
|
|
100
|
+
parts: msg.parts.map((p) =>
|
|
101
|
+
p.type === "tool_call" && p.callId === event.call_id
|
|
102
|
+
? { ...p, result: event.output }
|
|
103
|
+
: p
|
|
104
|
+
),
|
|
105
|
+
}))
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom Content Parts
|
|
113
|
+
|
|
114
|
+
Extend with custom types using generics:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
type ImagePart = { type: "image"; url: string };
|
|
118
|
+
type MyPart = ContentPart | ImagePart;
|
|
119
|
+
|
|
120
|
+
const { messages } = useStreamChat<MyPart>({
|
|
121
|
+
api: "/api/chat",
|
|
122
|
+
onEvent: (event, { appendPart }) => {
|
|
123
|
+
if (event.type === "image") {
|
|
124
|
+
appendPart({ type: "image", url: event.url });
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Re-exports from `@deltakit/core`
|
|
131
|
+
|
|
132
|
+
This package re-exports everything from `@deltakit/core`, so you only need one import:
|
|
133
|
+
|
|
134
|
+
- `parseSSEStream` -- SSE stream parser
|
|
135
|
+
- `fromOpenAiAgents` -- OpenAI Agents SDK history converter
|
|
136
|
+
- All types: `Message`, `ContentPart`, `TextPart`, `ToolCallPart`, `ReasoningPart`, `SSEEvent`, `TextDeltaEvent`, `ToolCallEvent`, `ToolResultEvent`
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
Full documentation, guides, and examples at [deltakit.dev](https://deltakit.dev).
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -22,12 +22,93 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
fromOpenAiAgents: () => import_core2.fromOpenAiAgents,
|
|
24
24
|
parseSSEStream: () => import_core2.parseSSEStream,
|
|
25
|
+
useAutoScroll: () => useAutoScroll,
|
|
25
26
|
useStreamChat: () => useStreamChat
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var import_core2 = require("@deltakit/core");
|
|
28
30
|
|
|
29
|
-
// src/use-
|
|
31
|
+
// src/use-auto-scroll.ts
|
|
30
32
|
var import_react = require("react");
|
|
33
|
+
var DEFAULT_THRESHOLD = 50;
|
|
34
|
+
function useAutoScroll(dependencies, options) {
|
|
35
|
+
const {
|
|
36
|
+
behavior = "instant",
|
|
37
|
+
enabled = true,
|
|
38
|
+
threshold = DEFAULT_THRESHOLD
|
|
39
|
+
} = options ?? {};
|
|
40
|
+
const ref = (0, import_react.useRef)(null);
|
|
41
|
+
const isAtBottomRef = (0, import_react.useRef)(true);
|
|
42
|
+
const [isAtBottom, setIsAtBottom] = (0, import_react.useState)(true);
|
|
43
|
+
const rafRef = (0, import_react.useRef)(null);
|
|
44
|
+
const scheduleScroll = (0, import_react.useCallback)(() => {
|
|
45
|
+
if (rafRef.current != null) return;
|
|
46
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
47
|
+
rafRef.current = null;
|
|
48
|
+
const el = ref.current;
|
|
49
|
+
if (el && isAtBottomRef.current) {
|
|
50
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}, [behavior]);
|
|
54
|
+
(0, import_react.useEffect)(() => {
|
|
55
|
+
const el = ref.current;
|
|
56
|
+
if (!el || !enabled) return;
|
|
57
|
+
const handleScroll = () => {
|
|
58
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
|
|
59
|
+
isAtBottomRef.current = atBottom;
|
|
60
|
+
setIsAtBottom((prev) => prev === atBottom ? prev : atBottom);
|
|
61
|
+
};
|
|
62
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
63
|
+
return () => el.removeEventListener("scroll", handleScroll);
|
|
64
|
+
}, [enabled, threshold]);
|
|
65
|
+
(0, import_react.useEffect)(() => {
|
|
66
|
+
if (!enabled || !isAtBottomRef.current) return;
|
|
67
|
+
scheduleScroll();
|
|
68
|
+
}, dependencies);
|
|
69
|
+
(0, import_react.useEffect)(() => {
|
|
70
|
+
const el = ref.current;
|
|
71
|
+
if (!el || !enabled) return;
|
|
72
|
+
const resizeObserver = new ResizeObserver(scheduleScroll);
|
|
73
|
+
for (const child of el.children) {
|
|
74
|
+
resizeObserver.observe(child);
|
|
75
|
+
}
|
|
76
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
77
|
+
for (const mutation of mutations) {
|
|
78
|
+
for (const node of mutation.addedNodes) {
|
|
79
|
+
if (node instanceof Element) {
|
|
80
|
+
resizeObserver.observe(node);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
scheduleScroll();
|
|
85
|
+
});
|
|
86
|
+
mutationObserver.observe(el, { childList: true, subtree: true });
|
|
87
|
+
return () => {
|
|
88
|
+
resizeObserver.disconnect();
|
|
89
|
+
mutationObserver.disconnect();
|
|
90
|
+
};
|
|
91
|
+
}, [enabled, scheduleScroll]);
|
|
92
|
+
(0, import_react.useEffect)(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
if (rafRef.current != null) {
|
|
95
|
+
cancelAnimationFrame(rafRef.current);
|
|
96
|
+
rafRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, []);
|
|
100
|
+
const scrollToBottom = (0, import_react.useCallback)(() => {
|
|
101
|
+
const el = ref.current;
|
|
102
|
+
if (!el) return;
|
|
103
|
+
isAtBottomRef.current = true;
|
|
104
|
+
setIsAtBottom(true);
|
|
105
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
106
|
+
}, [behavior]);
|
|
107
|
+
return { ref, scrollToBottom, isAtBottom };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/use-stream-chat.ts
|
|
111
|
+
var import_react2 = require("react");
|
|
31
112
|
var import_core = require("@deltakit/core");
|
|
32
113
|
var counter = 0;
|
|
33
114
|
function generateId() {
|
|
@@ -52,15 +133,15 @@ function useStreamChat(options) {
|
|
|
52
133
|
onError,
|
|
53
134
|
onFinish
|
|
54
135
|
} = options;
|
|
55
|
-
const [messages, setMessages] = (0,
|
|
136
|
+
const [messages, setMessages] = (0, import_react2.useState)(
|
|
56
137
|
initialMessages ?? []
|
|
57
138
|
);
|
|
58
|
-
const [isLoading, setIsLoading] = (0,
|
|
59
|
-
const [error, setError] = (0,
|
|
60
|
-
const abortRef = (0,
|
|
61
|
-
const messagesRef = (0,
|
|
139
|
+
const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
|
|
140
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
141
|
+
const abortRef = (0, import_react2.useRef)(null);
|
|
142
|
+
const messagesRef = (0, import_react2.useRef)(messages);
|
|
62
143
|
messagesRef.current = messages;
|
|
63
|
-
const appendText = (0,
|
|
144
|
+
const appendText = (0, import_react2.useCallback)((delta) => {
|
|
64
145
|
setMessages((prev) => {
|
|
65
146
|
const last = prev[prev.length - 1];
|
|
66
147
|
if (!last || last.role !== "assistant") return prev;
|
|
@@ -79,7 +160,7 @@ function useStreamChat(options) {
|
|
|
79
160
|
return [...prev.slice(0, -1), updated];
|
|
80
161
|
});
|
|
81
162
|
}, []);
|
|
82
|
-
const appendPart = (0,
|
|
163
|
+
const appendPart = (0, import_react2.useCallback)((part) => {
|
|
83
164
|
setMessages((prev) => {
|
|
84
165
|
const last = prev[prev.length - 1];
|
|
85
166
|
if (!last || last.role !== "assistant") return prev;
|
|
@@ -90,12 +171,12 @@ function useStreamChat(options) {
|
|
|
90
171
|
return [...prev.slice(0, -1), updated];
|
|
91
172
|
});
|
|
92
173
|
}, []);
|
|
93
|
-
const stop = (0,
|
|
174
|
+
const stop = (0, import_react2.useCallback)(() => {
|
|
94
175
|
abortRef.current?.abort();
|
|
95
176
|
abortRef.current = null;
|
|
96
177
|
setIsLoading(false);
|
|
97
178
|
}, []);
|
|
98
|
-
const sendMessage = (0,
|
|
179
|
+
const sendMessage = (0, import_react2.useCallback)(
|
|
99
180
|
(text) => {
|
|
100
181
|
if (abortRef.current) {
|
|
101
182
|
return;
|
|
@@ -168,7 +249,7 @@ function useStreamChat(options) {
|
|
|
168
249
|
},
|
|
169
250
|
[api, headers, body, onEvent, onMessage, onError, onFinish, appendText, appendPart]
|
|
170
251
|
);
|
|
171
|
-
(0,
|
|
252
|
+
(0, import_react2.useEffect)(() => {
|
|
172
253
|
return () => {
|
|
173
254
|
abortRef.current?.abort();
|
|
174
255
|
abortRef.current = null;
|
|
@@ -183,13 +264,11 @@ function useStreamChat(options) {
|
|
|
183
264
|
setMessages
|
|
184
265
|
};
|
|
185
266
|
}
|
|
186
|
-
|
|
187
|
-
// src/index.ts
|
|
188
|
-
var import_core2 = require("@deltakit/core");
|
|
189
267
|
// Annotate the CommonJS export names for ESM import in node:
|
|
190
268
|
0 && (module.exports = {
|
|
191
269
|
fromOpenAiAgents,
|
|
192
270
|
parseSSEStream,
|
|
271
|
+
useAutoScroll,
|
|
193
272
|
useStreamChat
|
|
194
273
|
});
|
|
195
274
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/use-stream-chat.ts"],"sourcesContent":["export { useStreamChat } from \"./use-stream-chat\";\n\nexport type {\n EventHelpers,\n UseStreamChatOptions,\n UseStreamChatReturn,\n} from \"./types\";\n\n// Re-export core types so consumers only need to import from @deltakit/react\nexport type {\n TextPart,\n ToolCallPart,\n ReasoningPart,\n ContentPart,\n Message,\n TextDeltaEvent,\n ToolCallEvent,\n ToolResultEvent,\n SSEEvent,\n} from \"@deltakit/core\";\n\nexport { parseSSEStream, fromOpenAiAgents } from \"@deltakit/core\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { parseSSEStream } from \"@deltakit/core\";\nimport type {\n ContentPart,\n Message,\n SSEEvent,\n} from \"@deltakit/core\";\nimport type {\n EventHelpers,\n UseStreamChatOptions,\n UseStreamChatReturn,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nlet counter = 0;\n\nfunction generateId(): string {\n return `msg_${Date.now()}_${++counter}`;\n}\n\nfunction createMessage<TPart extends { type: string }>(\n role: Message[\"role\"],\n parts: TPart[],\n): Message<TPart> {\n return { id: generateId(), role, parts };\n}\n\n// ---------------------------------------------------------------------------\n// Default event handler — accumulates `text_delta` into the last\n// assistant message's parts.\n// ---------------------------------------------------------------------------\n\nfunction defaultOnEvent(\n event: SSEEvent,\n helpers: EventHelpers<ContentPart>,\n): void {\n if (event.type === \"text_delta\") {\n helpers.appendText(event.delta);\n }\n // Other event types (e.g. tool_call) are silently ignored by default.\n // Users can provide their own `onEvent` to handle them.\n}\n\n// ---------------------------------------------------------------------------\n// useStreamChat\n// ---------------------------------------------------------------------------\n\nexport function useStreamChat<\n TPart extends { type: string } = ContentPart,\n TEvent extends { type: string } = SSEEvent,\n>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart> {\n const {\n api,\n headers,\n body,\n initialMessages,\n onEvent,\n onMessage,\n onError,\n onFinish,\n } = options;\n\n const [messages, setMessages] = useState<Message<TPart>[]>(\n initialMessages ?? [],\n );\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const abortRef = useRef<AbortController | null>(null);\n\n // We use a ref for the latest messages so that callbacks created inside\n // `sendMessage` always see the current value without re-creating closures.\n const messagesRef = useRef<Message<TPart>[]>(messages);\n messagesRef.current = messages;\n\n // -----------------------------------------------------------------------\n // appendText — append a text delta to the last text part of the last\n // assistant message, or create a new text part if needed.\n // -----------------------------------------------------------------------\n\n const appendText = useCallback((delta: string) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const parts = [...last.parts];\n const lastPart = parts[parts.length - 1];\n\n if (lastPart && lastPart.type === \"text\" && \"text\" in lastPart) {\n // Append to existing text part\n const textPart = lastPart as { type: \"text\"; text: string };\n parts[parts.length - 1] = {\n ...lastPart,\n text: textPart.text + delta,\n } as unknown as TPart;\n } else {\n // Create a new text part\n parts.push({ type: \"text\", text: delta } as unknown as TPart);\n }\n\n const updated: Message<TPart> = { ...last, parts };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // appendPart — push a new content part to the last assistant message.\n // -----------------------------------------------------------------------\n\n const appendPart = useCallback((part: TPart) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const updated: Message<TPart> = {\n ...last,\n parts: [...last.parts, part],\n };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------------\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setIsLoading(false);\n }, []);\n\n // -----------------------------------------------------------------------\n // sendMessage\n // -----------------------------------------------------------------------\n\n const sendMessage = useCallback(\n (text: string) => {\n // Prevent sending while already streaming.\n if (abortRef.current) {\n return;\n }\n\n const userMessage = createMessage<TPart>(\"user\", [\n { type: \"text\", text } as unknown as TPart,\n ]);\n const assistantMessage = createMessage<TPart>(\"assistant\", []);\n\n setMessages((prev) => {\n const next = [...prev, userMessage, assistantMessage];\n messagesRef.current = next;\n return next;\n });\n\n onMessage?.(userMessage);\n\n setError(null);\n setIsLoading(true);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n const eventHandler =\n onEvent ??\n (defaultOnEvent as unknown as (\n event: TEvent,\n helpers: EventHelpers<TPart>,\n ) => void);\n const helpers: EventHelpers<TPart> = {\n appendText,\n appendPart,\n setMessages,\n };\n\n // Fire-and-forget async IIFE — state is managed via React setState.\n (async () => {\n try {\n const response = await fetch(api, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify({ message: text, ...body }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(\n `SSE request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n if (!response.body) {\n throw new Error(\n \"Response body is null — SSE streaming not supported\",\n );\n }\n\n for await (const event of parseSSEStream(\n response.body,\n controller.signal,\n )) {\n eventHandler(event as unknown as TEvent, helpers);\n }\n\n // Stream finished — notify via callbacks.\n const finalMessages = messagesRef.current;\n const lastMessage = finalMessages[finalMessages.length - 1];\n\n if (lastMessage?.role === \"assistant\") {\n onMessage?.(lastMessage);\n }\n\n onFinish?.(finalMessages);\n } catch (err) {\n // AbortError is expected when the user calls `stop()`.\n if (err instanceof DOMException && err.name === \"AbortError\") {\n return;\n }\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n setError(error);\n onError?.(error);\n } finally {\n abortRef.current = null;\n setIsLoading(false);\n }\n })();\n },\n [api, headers, body, onEvent, onMessage, onError, onFinish, appendText, appendPart],\n );\n\n // -----------------------------------------------------------------------\n // Cleanup — abort any in-flight stream when the component unmounts.\n // -----------------------------------------------------------------------\n\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n abortRef.current = null;\n };\n }, []);\n\n return {\n messages,\n isLoading,\n error,\n sendMessage,\n stop,\n setMessages,\n };\n}\n"],"mappings":"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","names":["error","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/use-auto-scroll.ts","../src/use-stream-chat.ts"],"sourcesContent":["// Re-export core types so consumers only need to import from @deltakit/react\nexport type {\n\tContentPart,\n\tMessage,\n\tReasoningPart,\n\tSSEEvent,\n\tTextDeltaEvent,\n\tTextPart,\n\tToolCallEvent,\n\tToolCallPart,\n\tToolResultEvent,\n} from \"@deltakit/core\";\nexport { fromOpenAiAgents, parseSSEStream } from \"@deltakit/core\";\n\nexport type {\n\tEventHelpers,\n\tUseAutoScrollOptions,\n\tUseAutoScrollReturn,\n\tUseStreamChatOptions,\n\tUseStreamChatReturn,\n} from \"./types\";\nexport { useAutoScroll } from \"./use-auto-scroll\";\nexport { useStreamChat } from \"./use-stream-chat\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { UseAutoScrollOptions, UseAutoScrollReturn } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_THRESHOLD = 50;\n\n// ---------------------------------------------------------------------------\n// useAutoScroll\n// ---------------------------------------------------------------------------\n\nexport function useAutoScroll<T extends HTMLElement = HTMLDivElement>(\n\tdependencies: unknown[],\n\toptions?: UseAutoScrollOptions,\n): UseAutoScrollReturn<T> {\n\tconst {\n\t\tbehavior = \"instant\",\n\t\tenabled = true,\n\t\tthreshold = DEFAULT_THRESHOLD,\n\t} = options ?? {};\n\n\tconst ref = useRef<T | null>(null);\n\tconst isAtBottomRef = useRef(true);\n\tconst [isAtBottom, setIsAtBottom] = useState(true);\n\n\t// A single rAF id shared across all scroll sources — ensures we never\n\t// call scrollTo() more than once per frame, no matter how many\n\t// MutationObserver / ResizeObserver callbacks fire.\n\tconst rafRef = useRef<number | null>(null);\n\n\tconst scheduleScroll = useCallback(() => {\n\t\tif (rafRef.current != null) return;\n\t\trafRef.current = requestAnimationFrame(() => {\n\t\t\trafRef.current = null;\n\t\t\tconst el = ref.current;\n\t\t\tif (el && isAtBottomRef.current) {\n\t\t\t\tel.scrollTo({ top: el.scrollHeight, behavior });\n\t\t\t}\n\t\t});\n\t}, [behavior]);\n\n\t// -----------------------------------------------------------------------\n\t// Track whether the user is near the bottom via scroll events.\n\t// Only triggers a React re-render when the boolean actually changes.\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el || !enabled) return;\n\n\t\tconst handleScroll = () => {\n\t\t\tconst atBottom =\n\t\t\t\tel.scrollHeight - el.scrollTop - el.clientHeight <= threshold;\n\t\t\tisAtBottomRef.current = atBottom;\n\t\t\tsetIsAtBottom((prev) => (prev === atBottom ? prev : atBottom));\n\t\t};\n\n\t\tel.addEventListener(\"scroll\", handleScroll, { passive: true });\n\t\treturn () => el.removeEventListener(\"scroll\", handleScroll);\n\t}, [enabled, threshold]);\n\n\t// -----------------------------------------------------------------------\n\t// Scroll to bottom when dependencies change (if pinned).\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\tif (!enabled || !isAtBottomRef.current) return;\n\t\tscheduleScroll();\n\t\t// biome-ignore lint/correctness/useExhaustiveDependencies: dependencies are passed dynamically by the consumer\n\t}, dependencies);\n\n\t// -----------------------------------------------------------------------\n\t// MutationObserver + ResizeObserver — catch content changes during\n\t// streaming that happen between React re-renders (e.g. DOM mutations\n\t// from markdown renderers). Scroll calls are batched via rAF so we\n\t// scroll at most once per frame.\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el || !enabled) return;\n\n\t\tconst resizeObserver = new ResizeObserver(scheduleScroll);\n\n\t\t// Observe existing children for size changes.\n\t\tfor (const child of el.children) {\n\t\t\tresizeObserver.observe(child);\n\t\t}\n\n\t\t// Watch for new children added to the container.\n\t\tconst mutationObserver = new MutationObserver((mutations) => {\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tfor (const node of mutation.addedNodes) {\n\t\t\t\t\tif (node instanceof Element) {\n\t\t\t\t\t\tresizeObserver.observe(node);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tscheduleScroll();\n\t\t});\n\n\t\tmutationObserver.observe(el, { childList: true, subtree: true });\n\n\t\treturn () => {\n\t\t\tresizeObserver.disconnect();\n\t\t\tmutationObserver.disconnect();\n\t\t};\n\t}, [enabled, scheduleScroll]);\n\n\t// -----------------------------------------------------------------------\n\t// Cancel any pending rAF on unmount.\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (rafRef.current != null) {\n\t\t\t\tcancelAnimationFrame(rafRef.current);\n\t\t\t\trafRef.current = null;\n\t\t\t}\n\t\t};\n\t}, []);\n\n\t// -----------------------------------------------------------------------\n\t// scrollToBottom — imperative function that scrolls to the bottom and\n\t// re-pins auto-scroll.\n\t// -----------------------------------------------------------------------\n\n\tconst scrollToBottom = useCallback(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tisAtBottomRef.current = true;\n\t\tsetIsAtBottom(true);\n\t\tel.scrollTo({ top: el.scrollHeight, behavior });\n\t}, [behavior]);\n\n\treturn { ref, scrollToBottom, isAtBottom };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { parseSSEStream } from \"@deltakit/core\";\nimport type {\n ContentPart,\n Message,\n SSEEvent,\n} from \"@deltakit/core\";\nimport type {\n EventHelpers,\n UseStreamChatOptions,\n UseStreamChatReturn,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nlet counter = 0;\n\nfunction generateId(): string {\n return `msg_${Date.now()}_${++counter}`;\n}\n\nfunction createMessage<TPart extends { type: string }>(\n role: Message[\"role\"],\n parts: TPart[],\n): Message<TPart> {\n return { id: generateId(), role, parts };\n}\n\n// ---------------------------------------------------------------------------\n// Default event handler — accumulates `text_delta` into the last\n// assistant message's parts.\n// ---------------------------------------------------------------------------\n\nfunction defaultOnEvent(\n event: SSEEvent,\n helpers: EventHelpers<ContentPart>,\n): void {\n if (event.type === \"text_delta\") {\n helpers.appendText(event.delta);\n }\n // Other event types (e.g. tool_call) are silently ignored by default.\n // Users can provide their own `onEvent` to handle them.\n}\n\n// ---------------------------------------------------------------------------\n// useStreamChat\n// ---------------------------------------------------------------------------\n\nexport function useStreamChat<\n TPart extends { type: string } = ContentPart,\n TEvent extends { type: string } = SSEEvent,\n>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart> {\n const {\n api,\n headers,\n body,\n initialMessages,\n onEvent,\n onMessage,\n onError,\n onFinish,\n } = options;\n\n const [messages, setMessages] = useState<Message<TPart>[]>(\n initialMessages ?? [],\n );\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const abortRef = useRef<AbortController | null>(null);\n\n // We use a ref for the latest messages so that callbacks created inside\n // `sendMessage` always see the current value without re-creating closures.\n const messagesRef = useRef<Message<TPart>[]>(messages);\n messagesRef.current = messages;\n\n // -----------------------------------------------------------------------\n // appendText — append a text delta to the last text part of the last\n // assistant message, or create a new text part if needed.\n // -----------------------------------------------------------------------\n\n const appendText = useCallback((delta: string) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const parts = [...last.parts];\n const lastPart = parts[parts.length - 1];\n\n if (lastPart && lastPart.type === \"text\" && \"text\" in lastPart) {\n // Append to existing text part\n const textPart = lastPart as { type: \"text\"; text: string };\n parts[parts.length - 1] = {\n ...lastPart,\n text: textPart.text + delta,\n } as unknown as TPart;\n } else {\n // Create a new text part\n parts.push({ type: \"text\", text: delta } as unknown as TPart);\n }\n\n const updated: Message<TPart> = { ...last, parts };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // appendPart — push a new content part to the last assistant message.\n // -----------------------------------------------------------------------\n\n const appendPart = useCallback((part: TPart) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const updated: Message<TPart> = {\n ...last,\n parts: [...last.parts, part],\n };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------------\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setIsLoading(false);\n }, []);\n\n // -----------------------------------------------------------------------\n // sendMessage\n // -----------------------------------------------------------------------\n\n const sendMessage = useCallback(\n (text: string) => {\n // Prevent sending while already streaming.\n if (abortRef.current) {\n return;\n }\n\n const userMessage = createMessage<TPart>(\"user\", [\n { type: \"text\", text } as unknown as TPart,\n ]);\n const assistantMessage = createMessage<TPart>(\"assistant\", []);\n\n setMessages((prev) => {\n const next = [...prev, userMessage, assistantMessage];\n messagesRef.current = next;\n return next;\n });\n\n onMessage?.(userMessage);\n\n setError(null);\n setIsLoading(true);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n const eventHandler =\n onEvent ??\n (defaultOnEvent as unknown as (\n event: TEvent,\n helpers: EventHelpers<TPart>,\n ) => void);\n const helpers: EventHelpers<TPart> = {\n appendText,\n appendPart,\n setMessages,\n };\n\n // Fire-and-forget async IIFE — state is managed via React setState.\n (async () => {\n try {\n const response = await fetch(api, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify({ message: text, ...body }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(\n `SSE request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n if (!response.body) {\n throw new Error(\n \"Response body is null — SSE streaming not supported\",\n );\n }\n\n for await (const event of parseSSEStream(\n response.body,\n controller.signal,\n )) {\n eventHandler(event as unknown as TEvent, helpers);\n }\n\n // Stream finished — notify via callbacks.\n const finalMessages = messagesRef.current;\n const lastMessage = finalMessages[finalMessages.length - 1];\n\n if (lastMessage?.role === \"assistant\") {\n onMessage?.(lastMessage);\n }\n\n onFinish?.(finalMessages);\n } catch (err) {\n // AbortError is expected when the user calls `stop()`.\n if (err instanceof DOMException && err.name === \"AbortError\") {\n return;\n }\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n setError(error);\n onError?.(error);\n } finally {\n abortRef.current = null;\n setIsLoading(false);\n }\n })();\n },\n [api, headers, body, onEvent, onMessage, onError, onFinish, appendText, appendPart],\n );\n\n // -----------------------------------------------------------------------\n // Cleanup — abort any in-flight stream when the component unmounts.\n // -----------------------------------------------------------------------\n\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n abortRef.current = null;\n };\n }, []);\n\n return {\n messages,\n isLoading,\n error,\n sendMessage,\n stop,\n setMessages,\n };\n}\n"],"mappings":"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","names":["import_core","import_react","error"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ContentPart, Message, SSEEvent } from '@deltakit/core';
|
|
2
2
|
export { ContentPart, Message, ReasoningPart, SSEEvent, TextDeltaEvent, TextPart, ToolCallEvent, ToolCallPart, ToolResultEvent, fromOpenAiAgents, parseSSEStream } from '@deltakit/core';
|
|
3
|
-
import { Dispatch, SetStateAction } from 'react';
|
|
3
|
+
import { Dispatch, SetStateAction, RefObject } from 'react';
|
|
4
4
|
|
|
5
5
|
interface EventHelpers<TPart extends {
|
|
6
6
|
type: string;
|
|
@@ -57,6 +57,24 @@ interface UseStreamChatReturn<TPart extends {
|
|
|
57
57
|
/** Direct setter for programmatic message manipulation (clear, prepopulate, etc.). */
|
|
58
58
|
setMessages: Dispatch<SetStateAction<Message<TPart>[]>>;
|
|
59
59
|
}
|
|
60
|
+
interface UseAutoScrollOptions {
|
|
61
|
+
/** Scroll behavior when auto-scrolling. Default: `"instant"`. */
|
|
62
|
+
behavior?: ScrollBehavior;
|
|
63
|
+
/** Whether auto-scroll is enabled. Default: `true`. */
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
/** Distance (px) from the bottom to consider "at bottom". Default: `50`. */
|
|
66
|
+
threshold?: number;
|
|
67
|
+
}
|
|
68
|
+
interface UseAutoScrollReturn<T extends HTMLElement = HTMLDivElement> {
|
|
69
|
+
/** Attach this ref to your scrollable container element. */
|
|
70
|
+
ref: RefObject<T | null>;
|
|
71
|
+
/** Imperatively scroll to the bottom and re-pin auto-scroll. */
|
|
72
|
+
scrollToBottom: () => void;
|
|
73
|
+
/** Whether the scroll container is currently at/near the bottom. */
|
|
74
|
+
isAtBottom: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare function useAutoScroll<T extends HTMLElement = HTMLDivElement>(dependencies: unknown[], options?: UseAutoScrollOptions): UseAutoScrollReturn<T>;
|
|
60
78
|
|
|
61
79
|
declare function useStreamChat<TPart extends {
|
|
62
80
|
type: string;
|
|
@@ -64,4 +82,4 @@ declare function useStreamChat<TPart extends {
|
|
|
64
82
|
type: string;
|
|
65
83
|
} = SSEEvent>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart>;
|
|
66
84
|
|
|
67
|
-
export { type EventHelpers, type UseStreamChatOptions, type UseStreamChatReturn, useStreamChat };
|
|
85
|
+
export { type EventHelpers, type UseAutoScrollOptions, type UseAutoScrollReturn, type UseStreamChatOptions, type UseStreamChatReturn, useAutoScroll, useStreamChat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ContentPart, Message, SSEEvent } from '@deltakit/core';
|
|
2
2
|
export { ContentPart, Message, ReasoningPart, SSEEvent, TextDeltaEvent, TextPart, ToolCallEvent, ToolCallPart, ToolResultEvent, fromOpenAiAgents, parseSSEStream } from '@deltakit/core';
|
|
3
|
-
import { Dispatch, SetStateAction } from 'react';
|
|
3
|
+
import { Dispatch, SetStateAction, RefObject } from 'react';
|
|
4
4
|
|
|
5
5
|
interface EventHelpers<TPart extends {
|
|
6
6
|
type: string;
|
|
@@ -57,6 +57,24 @@ interface UseStreamChatReturn<TPart extends {
|
|
|
57
57
|
/** Direct setter for programmatic message manipulation (clear, prepopulate, etc.). */
|
|
58
58
|
setMessages: Dispatch<SetStateAction<Message<TPart>[]>>;
|
|
59
59
|
}
|
|
60
|
+
interface UseAutoScrollOptions {
|
|
61
|
+
/** Scroll behavior when auto-scrolling. Default: `"instant"`. */
|
|
62
|
+
behavior?: ScrollBehavior;
|
|
63
|
+
/** Whether auto-scroll is enabled. Default: `true`. */
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
/** Distance (px) from the bottom to consider "at bottom". Default: `50`. */
|
|
66
|
+
threshold?: number;
|
|
67
|
+
}
|
|
68
|
+
interface UseAutoScrollReturn<T extends HTMLElement = HTMLDivElement> {
|
|
69
|
+
/** Attach this ref to your scrollable container element. */
|
|
70
|
+
ref: RefObject<T | null>;
|
|
71
|
+
/** Imperatively scroll to the bottom and re-pin auto-scroll. */
|
|
72
|
+
scrollToBottom: () => void;
|
|
73
|
+
/** Whether the scroll container is currently at/near the bottom. */
|
|
74
|
+
isAtBottom: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare function useAutoScroll<T extends HTMLElement = HTMLDivElement>(dependencies: unknown[], options?: UseAutoScrollOptions): UseAutoScrollReturn<T>;
|
|
60
78
|
|
|
61
79
|
declare function useStreamChat<TPart extends {
|
|
62
80
|
type: string;
|
|
@@ -64,4 +82,4 @@ declare function useStreamChat<TPart extends {
|
|
|
64
82
|
type: string;
|
|
65
83
|
} = SSEEvent>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart>;
|
|
66
84
|
|
|
67
|
-
export { type EventHelpers, type UseStreamChatOptions, type UseStreamChatReturn, useStreamChat };
|
|
85
|
+
export { type EventHelpers, type UseAutoScrollOptions, type UseAutoScrollReturn, type UseStreamChatOptions, type UseStreamChatReturn, useAutoScroll, useStreamChat };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { fromOpenAiAgents, parseSSEStream as parseSSEStream2 } from "@deltakit/core";
|
|
3
|
+
|
|
4
|
+
// src/use-auto-scroll.ts
|
|
2
5
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
var DEFAULT_THRESHOLD = 50;
|
|
7
|
+
function useAutoScroll(dependencies, options) {
|
|
8
|
+
const {
|
|
9
|
+
behavior = "instant",
|
|
10
|
+
enabled = true,
|
|
11
|
+
threshold = DEFAULT_THRESHOLD
|
|
12
|
+
} = options ?? {};
|
|
13
|
+
const ref = useRef(null);
|
|
14
|
+
const isAtBottomRef = useRef(true);
|
|
15
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
16
|
+
const rafRef = useRef(null);
|
|
17
|
+
const scheduleScroll = useCallback(() => {
|
|
18
|
+
if (rafRef.current != null) return;
|
|
19
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
20
|
+
rafRef.current = null;
|
|
21
|
+
const el = ref.current;
|
|
22
|
+
if (el && isAtBottomRef.current) {
|
|
23
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}, [behavior]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const el = ref.current;
|
|
29
|
+
if (!el || !enabled) return;
|
|
30
|
+
const handleScroll = () => {
|
|
31
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
|
|
32
|
+
isAtBottomRef.current = atBottom;
|
|
33
|
+
setIsAtBottom((prev) => prev === atBottom ? prev : atBottom);
|
|
34
|
+
};
|
|
35
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
36
|
+
return () => el.removeEventListener("scroll", handleScroll);
|
|
37
|
+
}, [enabled, threshold]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!enabled || !isAtBottomRef.current) return;
|
|
40
|
+
scheduleScroll();
|
|
41
|
+
}, dependencies);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const el = ref.current;
|
|
44
|
+
if (!el || !enabled) return;
|
|
45
|
+
const resizeObserver = new ResizeObserver(scheduleScroll);
|
|
46
|
+
for (const child of el.children) {
|
|
47
|
+
resizeObserver.observe(child);
|
|
48
|
+
}
|
|
49
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
50
|
+
for (const mutation of mutations) {
|
|
51
|
+
for (const node of mutation.addedNodes) {
|
|
52
|
+
if (node instanceof Element) {
|
|
53
|
+
resizeObserver.observe(node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
scheduleScroll();
|
|
58
|
+
});
|
|
59
|
+
mutationObserver.observe(el, { childList: true, subtree: true });
|
|
60
|
+
return () => {
|
|
61
|
+
resizeObserver.disconnect();
|
|
62
|
+
mutationObserver.disconnect();
|
|
63
|
+
};
|
|
64
|
+
}, [enabled, scheduleScroll]);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
if (rafRef.current != null) {
|
|
68
|
+
cancelAnimationFrame(rafRef.current);
|
|
69
|
+
rafRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
const scrollToBottom = useCallback(() => {
|
|
74
|
+
const el = ref.current;
|
|
75
|
+
if (!el) return;
|
|
76
|
+
isAtBottomRef.current = true;
|
|
77
|
+
setIsAtBottom(true);
|
|
78
|
+
el.scrollTo({ top: el.scrollHeight, behavior });
|
|
79
|
+
}, [behavior]);
|
|
80
|
+
return { ref, scrollToBottom, isAtBottom };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/use-stream-chat.ts
|
|
84
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
3
85
|
import { parseSSEStream } from "@deltakit/core";
|
|
4
86
|
var counter = 0;
|
|
5
87
|
function generateId() {
|
|
@@ -24,15 +106,15 @@ function useStreamChat(options) {
|
|
|
24
106
|
onError,
|
|
25
107
|
onFinish
|
|
26
108
|
} = options;
|
|
27
|
-
const [messages, setMessages] =
|
|
109
|
+
const [messages, setMessages] = useState2(
|
|
28
110
|
initialMessages ?? []
|
|
29
111
|
);
|
|
30
|
-
const [isLoading, setIsLoading] =
|
|
31
|
-
const [error, setError] =
|
|
32
|
-
const abortRef =
|
|
33
|
-
const messagesRef =
|
|
112
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
113
|
+
const [error, setError] = useState2(null);
|
|
114
|
+
const abortRef = useRef2(null);
|
|
115
|
+
const messagesRef = useRef2(messages);
|
|
34
116
|
messagesRef.current = messages;
|
|
35
|
-
const appendText =
|
|
117
|
+
const appendText = useCallback2((delta) => {
|
|
36
118
|
setMessages((prev) => {
|
|
37
119
|
const last = prev[prev.length - 1];
|
|
38
120
|
if (!last || last.role !== "assistant") return prev;
|
|
@@ -51,7 +133,7 @@ function useStreamChat(options) {
|
|
|
51
133
|
return [...prev.slice(0, -1), updated];
|
|
52
134
|
});
|
|
53
135
|
}, []);
|
|
54
|
-
const appendPart =
|
|
136
|
+
const appendPart = useCallback2((part) => {
|
|
55
137
|
setMessages((prev) => {
|
|
56
138
|
const last = prev[prev.length - 1];
|
|
57
139
|
if (!last || last.role !== "assistant") return prev;
|
|
@@ -62,12 +144,12 @@ function useStreamChat(options) {
|
|
|
62
144
|
return [...prev.slice(0, -1), updated];
|
|
63
145
|
});
|
|
64
146
|
}, []);
|
|
65
|
-
const stop =
|
|
147
|
+
const stop = useCallback2(() => {
|
|
66
148
|
abortRef.current?.abort();
|
|
67
149
|
abortRef.current = null;
|
|
68
150
|
setIsLoading(false);
|
|
69
151
|
}, []);
|
|
70
|
-
const sendMessage =
|
|
152
|
+
const sendMessage = useCallback2(
|
|
71
153
|
(text) => {
|
|
72
154
|
if (abortRef.current) {
|
|
73
155
|
return;
|
|
@@ -140,7 +222,7 @@ function useStreamChat(options) {
|
|
|
140
222
|
},
|
|
141
223
|
[api, headers, body, onEvent, onMessage, onError, onFinish, appendText, appendPart]
|
|
142
224
|
);
|
|
143
|
-
|
|
225
|
+
useEffect2(() => {
|
|
144
226
|
return () => {
|
|
145
227
|
abortRef.current?.abort();
|
|
146
228
|
abortRef.current = null;
|
|
@@ -155,12 +237,10 @@ function useStreamChat(options) {
|
|
|
155
237
|
setMessages
|
|
156
238
|
};
|
|
157
239
|
}
|
|
158
|
-
|
|
159
|
-
// src/index.ts
|
|
160
|
-
import { parseSSEStream as parseSSEStream2, fromOpenAiAgents } from "@deltakit/core";
|
|
161
240
|
export {
|
|
162
241
|
fromOpenAiAgents,
|
|
163
242
|
parseSSEStream2 as parseSSEStream,
|
|
243
|
+
useAutoScroll,
|
|
164
244
|
useStreamChat
|
|
165
245
|
};
|
|
166
246
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/use-stream-chat.ts","../src/index.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { parseSSEStream } from \"@deltakit/core\";\nimport type {\n ContentPart,\n Message,\n SSEEvent,\n} from \"@deltakit/core\";\nimport type {\n EventHelpers,\n UseStreamChatOptions,\n UseStreamChatReturn,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nlet counter = 0;\n\nfunction generateId(): string {\n return `msg_${Date.now()}_${++counter}`;\n}\n\nfunction createMessage<TPart extends { type: string }>(\n role: Message[\"role\"],\n parts: TPart[],\n): Message<TPart> {\n return { id: generateId(), role, parts };\n}\n\n// ---------------------------------------------------------------------------\n// Default event handler — accumulates `text_delta` into the last\n// assistant message's parts.\n// ---------------------------------------------------------------------------\n\nfunction defaultOnEvent(\n event: SSEEvent,\n helpers: EventHelpers<ContentPart>,\n): void {\n if (event.type === \"text_delta\") {\n helpers.appendText(event.delta);\n }\n // Other event types (e.g. tool_call) are silently ignored by default.\n // Users can provide their own `onEvent` to handle them.\n}\n\n// ---------------------------------------------------------------------------\n// useStreamChat\n// ---------------------------------------------------------------------------\n\nexport function useStreamChat<\n TPart extends { type: string } = ContentPart,\n TEvent extends { type: string } = SSEEvent,\n>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart> {\n const {\n api,\n headers,\n body,\n initialMessages,\n onEvent,\n onMessage,\n onError,\n onFinish,\n } = options;\n\n const [messages, setMessages] = useState<Message<TPart>[]>(\n initialMessages ?? [],\n );\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const abortRef = useRef<AbortController | null>(null);\n\n // We use a ref for the latest messages so that callbacks created inside\n // `sendMessage` always see the current value without re-creating closures.\n const messagesRef = useRef<Message<TPart>[]>(messages);\n messagesRef.current = messages;\n\n // -----------------------------------------------------------------------\n // appendText — append a text delta to the last text part of the last\n // assistant message, or create a new text part if needed.\n // -----------------------------------------------------------------------\n\n const appendText = useCallback((delta: string) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const parts = [...last.parts];\n const lastPart = parts[parts.length - 1];\n\n if (lastPart && lastPart.type === \"text\" && \"text\" in lastPart) {\n // Append to existing text part\n const textPart = lastPart as { type: \"text\"; text: string };\n parts[parts.length - 1] = {\n ...lastPart,\n text: textPart.text + delta,\n } as unknown as TPart;\n } else {\n // Create a new text part\n parts.push({ type: \"text\", text: delta } as unknown as TPart);\n }\n\n const updated: Message<TPart> = { ...last, parts };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // appendPart — push a new content part to the last assistant message.\n // -----------------------------------------------------------------------\n\n const appendPart = useCallback((part: TPart) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const updated: Message<TPart> = {\n ...last,\n parts: [...last.parts, part],\n };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------------\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setIsLoading(false);\n }, []);\n\n // -----------------------------------------------------------------------\n // sendMessage\n // -----------------------------------------------------------------------\n\n const sendMessage = useCallback(\n (text: string) => {\n // Prevent sending while already streaming.\n if (abortRef.current) {\n return;\n }\n\n const userMessage = createMessage<TPart>(\"user\", [\n { type: \"text\", text } as unknown as TPart,\n ]);\n const assistantMessage = createMessage<TPart>(\"assistant\", []);\n\n setMessages((prev) => {\n const next = [...prev, userMessage, assistantMessage];\n messagesRef.current = next;\n return next;\n });\n\n onMessage?.(userMessage);\n\n setError(null);\n setIsLoading(true);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n const eventHandler =\n onEvent ??\n (defaultOnEvent as unknown as (\n event: TEvent,\n helpers: EventHelpers<TPart>,\n ) => void);\n const helpers: EventHelpers<TPart> = {\n appendText,\n appendPart,\n setMessages,\n };\n\n // Fire-and-forget async IIFE — state is managed via React setState.\n (async () => {\n try {\n const response = await fetch(api, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify({ message: text, ...body }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(\n `SSE request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n if (!response.body) {\n throw new Error(\n \"Response body is null — SSE streaming not supported\",\n );\n }\n\n for await (const event of parseSSEStream(\n response.body,\n controller.signal,\n )) {\n eventHandler(event as unknown as TEvent, helpers);\n }\n\n // Stream finished — notify via callbacks.\n const finalMessages = messagesRef.current;\n const lastMessage = finalMessages[finalMessages.length - 1];\n\n if (lastMessage?.role === \"assistant\") {\n onMessage?.(lastMessage);\n }\n\n onFinish?.(finalMessages);\n } catch (err) {\n // AbortError is expected when the user calls `stop()`.\n if (err instanceof DOMException && err.name === \"AbortError\") {\n return;\n }\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n setError(error);\n onError?.(error);\n } finally {\n abortRef.current = null;\n setIsLoading(false);\n }\n })();\n },\n [api, headers, body, onEvent, onMessage, onError, onFinish, appendText, appendPart],\n );\n\n // -----------------------------------------------------------------------\n // Cleanup — abort any in-flight stream when the component unmounts.\n // -----------------------------------------------------------------------\n\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n abortRef.current = null;\n };\n }, []);\n\n return {\n messages,\n isLoading,\n error,\n sendMessage,\n stop,\n setMessages,\n };\n}\n","export { useStreamChat } from \"./use-stream-chat\";\n\nexport type {\n EventHelpers,\n UseStreamChatOptions,\n UseStreamChatReturn,\n} from \"./types\";\n\n// Re-export core types so consumers only need to import from @deltakit/react\nexport type {\n TextPart,\n ToolCallPart,\n ReasoningPart,\n ContentPart,\n Message,\n TextDeltaEvent,\n ToolCallEvent,\n ToolResultEvent,\n SSEEvent,\n} from \"@deltakit/core\";\n\nexport { parseSSEStream, fromOpenAiAgents } from \"@deltakit/core\";\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,SAAS,sBAAsB;AAgB/B,IAAI,UAAU;AAEd,SAAS,aAAqB;AAC5B,SAAO,OAAO,KAAK,IAAI,CAAC,IAAI,EAAE,OAAO;AACvC;AAEA,SAAS,cACP,MACA,OACgB;AAChB,SAAO,EAAE,IAAI,WAAW,GAAG,MAAM,MAAM;AACzC;AAOA,SAAS,eACP,OACA,SACM;AACN,MAAI,MAAM,SAAS,cAAc;AAC/B,YAAQ,WAAW,MAAM,KAAK;AAAA,EAChC;AAGF;AAMO,SAAS,cAGd,SAA0E;AAC1E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,CAAC,UAAU,WAAW,IAAI;AAAA,IAC9B,mBAAmB,CAAC;AAAA,EACtB;AACA,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AAErD,QAAM,WAAW,OAA+B,IAAI;AAIpD,QAAM,cAAc,OAAyB,QAAQ;AACrD,cAAY,UAAU;AAOtB,QAAM,aAAa,YAAY,CAAC,UAAkB;AAChD,gBAAY,CAAC,SAAS;AACpB,YAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,UAAI,CAAC,QAAQ,KAAK,SAAS,YAAa,QAAO;AAE/C,YAAM,QAAQ,CAAC,GAAG,KAAK,KAAK;AAC5B,YAAM,WAAW,MAAM,MAAM,SAAS,CAAC;AAEvC,UAAI,YAAY,SAAS,SAAS,UAAU,UAAU,UAAU;AAE9D,cAAM,WAAW;AACjB,cAAM,MAAM,SAAS,CAAC,IAAI;AAAA,UACxB,GAAG;AAAA,UACH,MAAM,SAAS,OAAO;AAAA,QACxB;AAAA,MACF,OAAO;AAEL,cAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,MAAM,CAAqB;AAAA,MAC9D;AAEA,YAAM,UAA0B,EAAE,GAAG,MAAM,MAAM;AACjD,aAAO,CAAC,GAAG,KAAK,MAAM,GAAG,EAAE,GAAG,OAAO;AAAA,IACvC,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAML,QAAM,aAAa,YAAY,CAAC,SAAgB;AAC9C,gBAAY,CAAC,SAAS;AACpB,YAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,UAAI,CAAC,QAAQ,KAAK,SAAS,YAAa,QAAO;AAE/C,YAAM,UAA0B;AAAA,QAC9B,GAAG;AAAA,QACH,OAAO,CAAC,GAAG,KAAK,OAAO,IAAI;AAAA,MAC7B;AACA,aAAO,CAAC,GAAG,KAAK,MAAM,GAAG,EAAE,GAAG,OAAO;AAAA,IACvC,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAML,QAAM,OAAO,YAAY,MAAM;AAC7B,aAAS,SAAS,MAAM;AACxB,aAAS,UAAU;AACnB,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,CAAC;AAML,QAAM,cAAc;AAAA,IAClB,CAAC,SAAiB;AAEhB,UAAI,SAAS,SAAS;AACpB;AAAA,MACF;AAEA,YAAM,cAAc,cAAqB,QAAQ;AAAA,QAC/C,EAAE,MAAM,QAAQ,KAAK;AAAA,MACvB,CAAC;AACD,YAAM,mBAAmB,cAAqB,aAAa,CAAC,CAAC;AAE7D,kBAAY,CAAC,SAAS;AACpB,cAAM,OAAO,CAAC,GAAG,MAAM,aAAa,gBAAgB;AACpD,oBAAY,UAAU;AACtB,eAAO;AAAA,MACT,CAAC;AAED,kBAAY,WAAW;AAEvB,eAAS,IAAI;AACb,mBAAa,IAAI;AAEjB,YAAM,aAAa,IAAI,gBAAgB;AACvC,eAAS,UAAU;AAEnB,YAAM,eACJ,WACC;AAIH,YAAM,UAA+B;AAAA,QACnC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAGA,OAAC,YAAY;AACX,YAAI;AACF,gBAAM,WAAW,MAAM,MAAM,KAAK;AAAA,YAChC,QAAQ;AAAA,YACR,SAAS;AAAA,cACP,gBAAgB;AAAA,cAChB,GAAG;AAAA,YACL;AAAA,YACA,MAAM,KAAK,UAAU,EAAE,SAAS,MAAM,GAAG,KAAK,CAAC;AAAA,YAC/C,QAAQ,WAAW;AAAA,UACrB,CAAC;AAED,cAAI,CAAC,SAAS,IAAI;AAChB,kBAAM,IAAI;AAAA,cACR,uBAAuB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,YAC/D;AAAA,UACF;AAEA,cAAI,CAAC,SAAS,MAAM;AAClB,kBAAM,IAAI;AAAA,cACR;AAAA,YACF;AAAA,UACF;AAEA,2BAAiB,SAAS;AAAA,YACxB,SAAS;AAAA,YACT,WAAW;AAAA,UACb,GAAG;AACD,yBAAa,OAA4B,OAAO;AAAA,UAClD;AAGA,gBAAM,gBAAgB,YAAY;AAClC,gBAAM,cAAc,cAAc,cAAc,SAAS,CAAC;AAE1D,cAAI,aAAa,SAAS,aAAa;AACrC,wBAAY,WAAW;AAAA,UACzB;AAEA,qBAAW,aAAa;AAAA,QAC1B,SAAS,KAAK;AAEZ,cAAI,eAAe,gBAAgB,IAAI,SAAS,cAAc;AAC5D;AAAA,UACF;AAEA,gBAAMA,SAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAEhE,mBAASA,MAAK;AACd,oBAAUA,MAAK;AAAA,QACjB,UAAE;AACA,mBAAS,UAAU;AACnB,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF,GAAG;AAAA,IACL;AAAA,IACA,CAAC,KAAK,SAAS,MAAM,SAAS,WAAW,SAAS,UAAU,YAAY,UAAU;AAAA,EACpF;AAMA,YAAU,MAAM;AACd,WAAO,MAAM;AACX,eAAS,SAAS,MAAM;AACxB,eAAS,UAAU;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC3OA,SAAS,kBAAAC,iBAAgB,wBAAwB;","names":["error","parseSSEStream"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/use-auto-scroll.ts","../src/use-stream-chat.ts"],"sourcesContent":["// Re-export core types so consumers only need to import from @deltakit/react\nexport type {\n\tContentPart,\n\tMessage,\n\tReasoningPart,\n\tSSEEvent,\n\tTextDeltaEvent,\n\tTextPart,\n\tToolCallEvent,\n\tToolCallPart,\n\tToolResultEvent,\n} from \"@deltakit/core\";\nexport { fromOpenAiAgents, parseSSEStream } from \"@deltakit/core\";\n\nexport type {\n\tEventHelpers,\n\tUseAutoScrollOptions,\n\tUseAutoScrollReturn,\n\tUseStreamChatOptions,\n\tUseStreamChatReturn,\n} from \"./types\";\nexport { useAutoScroll } from \"./use-auto-scroll\";\nexport { useStreamChat } from \"./use-stream-chat\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { UseAutoScrollOptions, UseAutoScrollReturn } from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_THRESHOLD = 50;\n\n// ---------------------------------------------------------------------------\n// useAutoScroll\n// ---------------------------------------------------------------------------\n\nexport function useAutoScroll<T extends HTMLElement = HTMLDivElement>(\n\tdependencies: unknown[],\n\toptions?: UseAutoScrollOptions,\n): UseAutoScrollReturn<T> {\n\tconst {\n\t\tbehavior = \"instant\",\n\t\tenabled = true,\n\t\tthreshold = DEFAULT_THRESHOLD,\n\t} = options ?? {};\n\n\tconst ref = useRef<T | null>(null);\n\tconst isAtBottomRef = useRef(true);\n\tconst [isAtBottom, setIsAtBottom] = useState(true);\n\n\t// A single rAF id shared across all scroll sources — ensures we never\n\t// call scrollTo() more than once per frame, no matter how many\n\t// MutationObserver / ResizeObserver callbacks fire.\n\tconst rafRef = useRef<number | null>(null);\n\n\tconst scheduleScroll = useCallback(() => {\n\t\tif (rafRef.current != null) return;\n\t\trafRef.current = requestAnimationFrame(() => {\n\t\t\trafRef.current = null;\n\t\t\tconst el = ref.current;\n\t\t\tif (el && isAtBottomRef.current) {\n\t\t\t\tel.scrollTo({ top: el.scrollHeight, behavior });\n\t\t\t}\n\t\t});\n\t}, [behavior]);\n\n\t// -----------------------------------------------------------------------\n\t// Track whether the user is near the bottom via scroll events.\n\t// Only triggers a React re-render when the boolean actually changes.\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el || !enabled) return;\n\n\t\tconst handleScroll = () => {\n\t\t\tconst atBottom =\n\t\t\t\tel.scrollHeight - el.scrollTop - el.clientHeight <= threshold;\n\t\t\tisAtBottomRef.current = atBottom;\n\t\t\tsetIsAtBottom((prev) => (prev === atBottom ? prev : atBottom));\n\t\t};\n\n\t\tel.addEventListener(\"scroll\", handleScroll, { passive: true });\n\t\treturn () => el.removeEventListener(\"scroll\", handleScroll);\n\t}, [enabled, threshold]);\n\n\t// -----------------------------------------------------------------------\n\t// Scroll to bottom when dependencies change (if pinned).\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\tif (!enabled || !isAtBottomRef.current) return;\n\t\tscheduleScroll();\n\t\t// biome-ignore lint/correctness/useExhaustiveDependencies: dependencies are passed dynamically by the consumer\n\t}, dependencies);\n\n\t// -----------------------------------------------------------------------\n\t// MutationObserver + ResizeObserver — catch content changes during\n\t// streaming that happen between React re-renders (e.g. DOM mutations\n\t// from markdown renderers). Scroll calls are batched via rAF so we\n\t// scroll at most once per frame.\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el || !enabled) return;\n\n\t\tconst resizeObserver = new ResizeObserver(scheduleScroll);\n\n\t\t// Observe existing children for size changes.\n\t\tfor (const child of el.children) {\n\t\t\tresizeObserver.observe(child);\n\t\t}\n\n\t\t// Watch for new children added to the container.\n\t\tconst mutationObserver = new MutationObserver((mutations) => {\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tfor (const node of mutation.addedNodes) {\n\t\t\t\t\tif (node instanceof Element) {\n\t\t\t\t\t\tresizeObserver.observe(node);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tscheduleScroll();\n\t\t});\n\n\t\tmutationObserver.observe(el, { childList: true, subtree: true });\n\n\t\treturn () => {\n\t\t\tresizeObserver.disconnect();\n\t\t\tmutationObserver.disconnect();\n\t\t};\n\t}, [enabled, scheduleScroll]);\n\n\t// -----------------------------------------------------------------------\n\t// Cancel any pending rAF on unmount.\n\t// -----------------------------------------------------------------------\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (rafRef.current != null) {\n\t\t\t\tcancelAnimationFrame(rafRef.current);\n\t\t\t\trafRef.current = null;\n\t\t\t}\n\t\t};\n\t}, []);\n\n\t// -----------------------------------------------------------------------\n\t// scrollToBottom — imperative function that scrolls to the bottom and\n\t// re-pins auto-scroll.\n\t// -----------------------------------------------------------------------\n\n\tconst scrollToBottom = useCallback(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tisAtBottomRef.current = true;\n\t\tsetIsAtBottom(true);\n\t\tel.scrollTo({ top: el.scrollHeight, behavior });\n\t}, [behavior]);\n\n\treturn { ref, scrollToBottom, isAtBottom };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { parseSSEStream } from \"@deltakit/core\";\nimport type {\n ContentPart,\n Message,\n SSEEvent,\n} from \"@deltakit/core\";\nimport type {\n EventHelpers,\n UseStreamChatOptions,\n UseStreamChatReturn,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nlet counter = 0;\n\nfunction generateId(): string {\n return `msg_${Date.now()}_${++counter}`;\n}\n\nfunction createMessage<TPart extends { type: string }>(\n role: Message[\"role\"],\n parts: TPart[],\n): Message<TPart> {\n return { id: generateId(), role, parts };\n}\n\n// ---------------------------------------------------------------------------\n// Default event handler — accumulates `text_delta` into the last\n// assistant message's parts.\n// ---------------------------------------------------------------------------\n\nfunction defaultOnEvent(\n event: SSEEvent,\n helpers: EventHelpers<ContentPart>,\n): void {\n if (event.type === \"text_delta\") {\n helpers.appendText(event.delta);\n }\n // Other event types (e.g. tool_call) are silently ignored by default.\n // Users can provide their own `onEvent` to handle them.\n}\n\n// ---------------------------------------------------------------------------\n// useStreamChat\n// ---------------------------------------------------------------------------\n\nexport function useStreamChat<\n TPart extends { type: string } = ContentPart,\n TEvent extends { type: string } = SSEEvent,\n>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart> {\n const {\n api,\n headers,\n body,\n initialMessages,\n onEvent,\n onMessage,\n onError,\n onFinish,\n } = options;\n\n const [messages, setMessages] = useState<Message<TPart>[]>(\n initialMessages ?? [],\n );\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const abortRef = useRef<AbortController | null>(null);\n\n // We use a ref for the latest messages so that callbacks created inside\n // `sendMessage` always see the current value without re-creating closures.\n const messagesRef = useRef<Message<TPart>[]>(messages);\n messagesRef.current = messages;\n\n // -----------------------------------------------------------------------\n // appendText — append a text delta to the last text part of the last\n // assistant message, or create a new text part if needed.\n // -----------------------------------------------------------------------\n\n const appendText = useCallback((delta: string) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const parts = [...last.parts];\n const lastPart = parts[parts.length - 1];\n\n if (lastPart && lastPart.type === \"text\" && \"text\" in lastPart) {\n // Append to existing text part\n const textPart = lastPart as { type: \"text\"; text: string };\n parts[parts.length - 1] = {\n ...lastPart,\n text: textPart.text + delta,\n } as unknown as TPart;\n } else {\n // Create a new text part\n parts.push({ type: \"text\", text: delta } as unknown as TPart);\n }\n\n const updated: Message<TPart> = { ...last, parts };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // appendPart — push a new content part to the last assistant message.\n // -----------------------------------------------------------------------\n\n const appendPart = useCallback((part: TPart) => {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n const updated: Message<TPart> = {\n ...last,\n parts: [...last.parts, part],\n };\n return [...prev.slice(0, -1), updated];\n });\n }, []);\n\n // -----------------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------------\n\n const stop = useCallback(() => {\n abortRef.current?.abort();\n abortRef.current = null;\n setIsLoading(false);\n }, []);\n\n // -----------------------------------------------------------------------\n // sendMessage\n // -----------------------------------------------------------------------\n\n const sendMessage = useCallback(\n (text: string) => {\n // Prevent sending while already streaming.\n if (abortRef.current) {\n return;\n }\n\n const userMessage = createMessage<TPart>(\"user\", [\n { type: \"text\", text } as unknown as TPart,\n ]);\n const assistantMessage = createMessage<TPart>(\"assistant\", []);\n\n setMessages((prev) => {\n const next = [...prev, userMessage, assistantMessage];\n messagesRef.current = next;\n return next;\n });\n\n onMessage?.(userMessage);\n\n setError(null);\n setIsLoading(true);\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n const eventHandler =\n onEvent ??\n (defaultOnEvent as unknown as (\n event: TEvent,\n helpers: EventHelpers<TPart>,\n ) => void);\n const helpers: EventHelpers<TPart> = {\n appendText,\n appendPart,\n setMessages,\n };\n\n // Fire-and-forget async IIFE — state is managed via React setState.\n (async () => {\n try {\n const response = await fetch(api, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify({ message: text, ...body }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(\n `SSE request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n if (!response.body) {\n throw new Error(\n \"Response body is null — SSE streaming not supported\",\n );\n }\n\n for await (const event of parseSSEStream(\n response.body,\n controller.signal,\n )) {\n eventHandler(event as unknown as TEvent, helpers);\n }\n\n // Stream finished — notify via callbacks.\n const finalMessages = messagesRef.current;\n const lastMessage = finalMessages[finalMessages.length - 1];\n\n if (lastMessage?.role === \"assistant\") {\n onMessage?.(lastMessage);\n }\n\n onFinish?.(finalMessages);\n } catch (err) {\n // AbortError is expected when the user calls `stop()`.\n if (err instanceof DOMException && err.name === \"AbortError\") {\n return;\n }\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n setError(error);\n onError?.(error);\n } finally {\n abortRef.current = null;\n setIsLoading(false);\n }\n })();\n },\n [api, headers, body, onEvent, onMessage, onError, onFinish, appendText, appendPart],\n );\n\n // -----------------------------------------------------------------------\n // Cleanup — abort any in-flight stream when the component unmounts.\n // -----------------------------------------------------------------------\n\n useEffect(() => {\n return () => {\n abortRef.current?.abort();\n abortRef.current = null;\n };\n }, []);\n\n return {\n messages,\n isLoading,\n error,\n sendMessage,\n stop,\n setMessages,\n };\n}\n"],"mappings":"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","names":["parseSSEStream","useCallback","useEffect","useRef","useState","error"]}
|
package/package.json
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
2
|
+
"name": "@deltakit/react",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "DeltaKit React Markdown for LLM Response",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "DeltaKit HQ",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/deltakithq/deltakit-monorepo",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"deltakit",
|
|
15
|
+
"react"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"check": "biome check src/",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"clean": "rm -rf dist"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@deltakit/core": "workspace:*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^18.3.18",
|
|
42
|
+
"react": "^18.3.1",
|
|
43
|
+
"tsup": "8.5.1",
|
|
44
|
+
"typescript": "^5.7.2"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18"
|
|
48
|
+
}
|
|
49
|
+
}
|