@futurity/chat-react 0.0.1
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 +153 -0
- package/dist/index.d.ts +414 -0
- package/dist/index.js +637 -0
- package/package.json +35 -0
- package/src/WebSocketConnection.ts +284 -0
- package/src/chat-protocol.ts +22 -0
- package/src/index.ts +39 -0
- package/src/tree-builder.ts +116 -0
- package/src/types.ts +63 -0
- package/src/useReconnectingWebSocket.ts +126 -0
- package/src/useStreamChat.ts +354 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @futurity/chat-react
|
|
2
|
+
|
|
3
|
+
React hooks and utilities for embedding Futurity chat in any React application.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @futurity/chat-react @futurity/chat-protocol react zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useStreamChat } from "@futurity/chat-react";
|
|
15
|
+
|
|
16
|
+
function Chat({ chatId, apiUrl }: { chatId: string; apiUrl: string }) {
|
|
17
|
+
const {
|
|
18
|
+
messages,
|
|
19
|
+
setMessages,
|
|
20
|
+
sendMessage,
|
|
21
|
+
status,
|
|
22
|
+
stop,
|
|
23
|
+
isConnected,
|
|
24
|
+
pendingClarify,
|
|
25
|
+
sendClarifyResponse,
|
|
26
|
+
} = useStreamChat({
|
|
27
|
+
chatId,
|
|
28
|
+
wsUrl: `${apiUrl}/api/v2/chat/${chatId}`,
|
|
29
|
+
onStart: (id) => {
|
|
30
|
+
// A new assistant message is about to stream
|
|
31
|
+
setMessages((prev) => [
|
|
32
|
+
...prev,
|
|
33
|
+
{ id, role: "assistant", parts: [{ type: "text", text: "" }] },
|
|
34
|
+
]);
|
|
35
|
+
},
|
|
36
|
+
onDelta: (id, delta, consolidated) => {
|
|
37
|
+
// Append or replace the latest part
|
|
38
|
+
setMessages((prev) =>
|
|
39
|
+
prev.map((m) => {
|
|
40
|
+
if (m.id !== id) return m;
|
|
41
|
+
const parts = m.parts ?? [];
|
|
42
|
+
return {
|
|
43
|
+
...m,
|
|
44
|
+
parts: consolidated
|
|
45
|
+
? [...parts.slice(0, -1), delta]
|
|
46
|
+
: [...parts, delta],
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
onResume: (id, parts) => {
|
|
52
|
+
setMessages((prev) =>
|
|
53
|
+
prev.map((m) => (m.id !== id ? m : { ...m, parts })),
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
onHistory: (history) => {
|
|
57
|
+
setMessages(history.initialPath);
|
|
58
|
+
},
|
|
59
|
+
onError: (error) => {
|
|
60
|
+
console.error("Chat error:", error.message ?? error.error);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
{messages.map((msg) => (
|
|
67
|
+
<div key={msg.id}>
|
|
68
|
+
<strong>{msg.role}:</strong>
|
|
69
|
+
{msg.parts
|
|
70
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
71
|
+
.map((p, i) => (
|
|
72
|
+
<span key={i}>{p.text}</span>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
|
|
77
|
+
{status === "streaming" && <button onClick={stop}>Stop</button>}
|
|
78
|
+
|
|
79
|
+
<form
|
|
80
|
+
onSubmit={async (e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
const input = e.currentTarget.elements.namedItem("msg") as HTMLInputElement;
|
|
83
|
+
await sendMessage({ parts: [{ type: "text", text: input.value }] });
|
|
84
|
+
input.value = "";
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<input name="msg" placeholder="Type a message..." />
|
|
88
|
+
<button type="submit" disabled={status === "streaming"}>
|
|
89
|
+
Send
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### `useStreamChat(options)`
|
|
100
|
+
|
|
101
|
+
The main hook for connecting to a Futurity chat WebSocket.
|
|
102
|
+
|
|
103
|
+
**Options:**
|
|
104
|
+
|
|
105
|
+
| Option | Type | Description |
|
|
106
|
+
| ---------- | ---------- | --------------------------------------------------------- |
|
|
107
|
+
| `chatId` | `string` | The chat ID to connect to |
|
|
108
|
+
| `wsUrl` | `string` | Full WebSocket URL (e.g. `wss://api.example.com/api/v2/chat/<id>`) |
|
|
109
|
+
| `onStart` | `function` | Called when a new assistant message starts |
|
|
110
|
+
| `onDelta` | `function` | Called on each stream delta |
|
|
111
|
+
| `onResume` | `function` | Called when a stream resumes with accumulated parts |
|
|
112
|
+
| `onFinish` | `function` | Called when streaming finishes |
|
|
113
|
+
| `onError` | `function` | Called on protocol errors |
|
|
114
|
+
| `onHistory`| `function` | Called when chat history is received |
|
|
115
|
+
|
|
116
|
+
**Returns:**
|
|
117
|
+
|
|
118
|
+
| Property | Type | Description |
|
|
119
|
+
| -------------------- | ----------------------- | -------------------------------------- |
|
|
120
|
+
| `messages` | `ChatMessage[]` | Current messages in the active branch |
|
|
121
|
+
| `setMessages` | `SetState<ChatMessage[]>` | Replace the message list |
|
|
122
|
+
| `sendMessage` | `(payload) => Promise` | Send a new user message |
|
|
123
|
+
| `injectMessage` | `(text) => Promise` | Inject text into an active stream |
|
|
124
|
+
| `status` | `ChatStatus` | `"ready"` / `"submitted"` / `"streaming"` / `"error"` |
|
|
125
|
+
| `stop` | `() => Promise` | Cancel the current generation |
|
|
126
|
+
| `reset` | `() => void` | Reset all chat state |
|
|
127
|
+
| `isConnected` | `boolean` | WebSocket connection status |
|
|
128
|
+
| `pendingClarify` | `object \| null` | Pending clarification request |
|
|
129
|
+
| `sendClarifyResponse`| `(answers) => void` | Submit clarification answers |
|
|
130
|
+
|
|
131
|
+
### `useReconnectingWebSocket(options)`
|
|
132
|
+
|
|
133
|
+
A generic WebSocket hook with automatic reconnection, heartbeat, and message queuing. Used internally by `useStreamChat`, but also available for custom WebSocket connections.
|
|
134
|
+
|
|
135
|
+
### Tree Utilities
|
|
136
|
+
|
|
137
|
+
- `buildTree(messages, byId)` - Build a navigable message tree from a flat array
|
|
138
|
+
- `findLatestPath(tree, byId)` - Find the latest conversation path in a tree
|
|
139
|
+
- `MessageNode` - Tree node class with `getChildren()`, `parent_id`, and `message`
|
|
140
|
+
|
|
141
|
+
### Protocol Types
|
|
142
|
+
|
|
143
|
+
All chat WebSocket protocol types are re-exported from `@futurity/chat-protocol`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import type {
|
|
147
|
+
WsClientCommand,
|
|
148
|
+
WsServerMessage,
|
|
149
|
+
WsStreamMessage,
|
|
150
|
+
WsClarifyQuestion,
|
|
151
|
+
WsVaultItem,
|
|
152
|
+
} from "@futurity/chat-react";
|
|
153
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { WsServerMessage, MessagePart, WsClarifyQuestion, WsVaultItem, WsStreamMessage, WsClarifyRequestMessage } from '@futurity/chat-protocol';
|
|
2
|
+
export { WsClientCommand as ClientCommand, MessagePart, WsClarifyQuestion, WsClientCommand, WsErrorMessage, WsServerMessage, WsStreamMessage, WsVaultItem } from '@futurity/chat-protocol';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Chat WebSocket protocol helpers.
|
|
7
|
+
*
|
|
8
|
+
* All protocol schemas live in @futurity/chat-protocol;
|
|
9
|
+
* this module provides a lightweight parse helper for the SDK.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
declare function parseServerMessage(data: unknown): WsServerMessage | null;
|
|
13
|
+
|
|
14
|
+
declare const messageMetadataSchema: z.ZodObject<{
|
|
15
|
+
parent_id: z.ZodNullable<z.ZodUUID>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
type MessageMetadata = z.infer<typeof messageMetadataSchema>;
|
|
18
|
+
/** A chat message suitable for rendering in a UI. */
|
|
19
|
+
type ChatMessage = {
|
|
20
|
+
id: string;
|
|
21
|
+
role: "user" | "assistant" | "system";
|
|
22
|
+
parts: MessagePart[];
|
|
23
|
+
createdAt?: Date;
|
|
24
|
+
metadata?: MessageMetadata;
|
|
25
|
+
};
|
|
26
|
+
declare const Z_ChatMessage: z.ZodObject<{
|
|
27
|
+
id: z.ZodString;
|
|
28
|
+
role: z.ZodEnum<{
|
|
29
|
+
user: "user";
|
|
30
|
+
assistant: "assistant";
|
|
31
|
+
system: "system";
|
|
32
|
+
}>;
|
|
33
|
+
parts: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
34
|
+
type: z.ZodLiteral<"text">;
|
|
35
|
+
text: z.ZodString;
|
|
36
|
+
state: z.ZodOptional<z.ZodEnum<{
|
|
37
|
+
streaming: "streaming";
|
|
38
|
+
done: "done";
|
|
39
|
+
}>>;
|
|
40
|
+
providerMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
41
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
42
|
+
type: z.ZodLiteral<"reasoning">;
|
|
43
|
+
text: z.ZodString;
|
|
44
|
+
state: z.ZodOptional<z.ZodEnum<{
|
|
45
|
+
streaming: "streaming";
|
|
46
|
+
done: "done";
|
|
47
|
+
}>>;
|
|
48
|
+
providerMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
49
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
50
|
+
type: z.ZodLiteral<"source-url">;
|
|
51
|
+
sourceId: z.ZodString;
|
|
52
|
+
url: z.ZodString;
|
|
53
|
+
title: z.ZodOptional<z.ZodString>;
|
|
54
|
+
providerMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
55
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
56
|
+
type: z.ZodLiteral<"source-document">;
|
|
57
|
+
sourceId: z.ZodString;
|
|
58
|
+
mediaType: z.ZodString;
|
|
59
|
+
title: z.ZodString;
|
|
60
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
61
|
+
providerMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
62
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
63
|
+
type: z.ZodLiteral<"file">;
|
|
64
|
+
mediaType: z.ZodString;
|
|
65
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
66
|
+
url: z.ZodString;
|
|
67
|
+
providerMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
68
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
69
|
+
type: z.ZodLiteral<"step-start">;
|
|
70
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
71
|
+
type: z.ZodCustom<`data-${string}`, `data-${string}`>;
|
|
72
|
+
id: z.ZodOptional<z.ZodString>;
|
|
73
|
+
data: z.ZodUnknown;
|
|
74
|
+
}, z.core.$strip>, z.ZodIntersection<z.ZodObject<{
|
|
75
|
+
type: z.ZodCustom<`tool-${string}`, `tool-${string}`>;
|
|
76
|
+
}, z.core.$strip>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
77
|
+
toolCallId: z.ZodString;
|
|
78
|
+
title: z.ZodOptional<z.ZodString>;
|
|
79
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
80
|
+
state: z.ZodLiteral<"input-streaming">;
|
|
81
|
+
input: z.ZodUnknown;
|
|
82
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
83
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
84
|
+
toolCallId: z.ZodString;
|
|
85
|
+
title: z.ZodOptional<z.ZodString>;
|
|
86
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
87
|
+
state: z.ZodLiteral<"input-available">;
|
|
88
|
+
input: z.ZodUnknown;
|
|
89
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
90
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
91
|
+
toolCallId: z.ZodString;
|
|
92
|
+
title: z.ZodOptional<z.ZodString>;
|
|
93
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
94
|
+
state: z.ZodLiteral<"approval-requested">;
|
|
95
|
+
input: z.ZodUnknown;
|
|
96
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
97
|
+
approval: z.ZodObject<{
|
|
98
|
+
id: z.ZodString;
|
|
99
|
+
}, z.core.$strip>;
|
|
100
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
101
|
+
toolCallId: z.ZodString;
|
|
102
|
+
title: z.ZodOptional<z.ZodString>;
|
|
103
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
104
|
+
state: z.ZodLiteral<"approval-responded">;
|
|
105
|
+
input: z.ZodUnknown;
|
|
106
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
107
|
+
approval: z.ZodObject<{
|
|
108
|
+
id: z.ZodString;
|
|
109
|
+
approved: z.ZodBoolean;
|
|
110
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
113
|
+
toolCallId: z.ZodString;
|
|
114
|
+
title: z.ZodOptional<z.ZodString>;
|
|
115
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
116
|
+
state: z.ZodLiteral<"output-available">;
|
|
117
|
+
input: z.ZodUnknown;
|
|
118
|
+
output: z.ZodUnknown;
|
|
119
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
120
|
+
preliminary: z.ZodOptional<z.ZodBoolean>;
|
|
121
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
122
|
+
id: z.ZodString;
|
|
123
|
+
approved: z.ZodLiteral<true>;
|
|
124
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
125
|
+
}, z.core.$strip>>;
|
|
126
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
127
|
+
toolCallId: z.ZodString;
|
|
128
|
+
title: z.ZodOptional<z.ZodString>;
|
|
129
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
130
|
+
state: z.ZodLiteral<"output-error">;
|
|
131
|
+
input: z.ZodUnknown;
|
|
132
|
+
rawInput: z.ZodOptional<z.ZodUnknown>;
|
|
133
|
+
errorText: z.ZodString;
|
|
134
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
135
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
136
|
+
id: z.ZodString;
|
|
137
|
+
approved: z.ZodLiteral<true>;
|
|
138
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
139
|
+
}, z.core.$strip>>;
|
|
140
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
141
|
+
toolCallId: z.ZodString;
|
|
142
|
+
title: z.ZodOptional<z.ZodString>;
|
|
143
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
144
|
+
state: z.ZodLiteral<"output-denied">;
|
|
145
|
+
input: z.ZodUnknown;
|
|
146
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
147
|
+
approval: z.ZodObject<{
|
|
148
|
+
id: z.ZodString;
|
|
149
|
+
approved: z.ZodLiteral<false>;
|
|
150
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
151
|
+
}, z.core.$strip>;
|
|
152
|
+
}, z.core.$strip>], "state">>, z.ZodIntersection<z.ZodObject<{
|
|
153
|
+
type: z.ZodLiteral<"dynamic-tool">;
|
|
154
|
+
toolName: z.ZodString;
|
|
155
|
+
toolCallId: z.ZodString;
|
|
156
|
+
title: z.ZodOptional<z.ZodString>;
|
|
157
|
+
providerExecuted: z.ZodOptional<z.ZodBoolean>;
|
|
158
|
+
}, z.core.$strip>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
159
|
+
state: z.ZodLiteral<"input-streaming">;
|
|
160
|
+
input: z.ZodUnknown;
|
|
161
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
162
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
163
|
+
state: z.ZodLiteral<"input-available">;
|
|
164
|
+
input: z.ZodUnknown;
|
|
165
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
166
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
167
|
+
state: z.ZodLiteral<"approval-requested">;
|
|
168
|
+
input: z.ZodUnknown;
|
|
169
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
170
|
+
approval: z.ZodObject<{
|
|
171
|
+
id: z.ZodString;
|
|
172
|
+
}, z.core.$strip>;
|
|
173
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
174
|
+
state: z.ZodLiteral<"approval-responded">;
|
|
175
|
+
input: z.ZodUnknown;
|
|
176
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
177
|
+
approval: z.ZodObject<{
|
|
178
|
+
id: z.ZodString;
|
|
179
|
+
approved: z.ZodBoolean;
|
|
180
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
181
|
+
}, z.core.$strip>;
|
|
182
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
183
|
+
state: z.ZodLiteral<"output-available">;
|
|
184
|
+
input: z.ZodUnknown;
|
|
185
|
+
output: z.ZodUnknown;
|
|
186
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
187
|
+
preliminary: z.ZodOptional<z.ZodBoolean>;
|
|
188
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
189
|
+
id: z.ZodString;
|
|
190
|
+
approved: z.ZodLiteral<true>;
|
|
191
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
192
|
+
}, z.core.$strip>>;
|
|
193
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
194
|
+
state: z.ZodLiteral<"output-error">;
|
|
195
|
+
input: z.ZodUnknown;
|
|
196
|
+
errorText: z.ZodString;
|
|
197
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
198
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
199
|
+
id: z.ZodString;
|
|
200
|
+
approved: z.ZodLiteral<true>;
|
|
201
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
202
|
+
}, z.core.$strip>>;
|
|
203
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
204
|
+
state: z.ZodLiteral<"output-denied">;
|
|
205
|
+
input: z.ZodUnknown;
|
|
206
|
+
callProviderMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodAny>>>;
|
|
207
|
+
approval: z.ZodObject<{
|
|
208
|
+
id: z.ZodString;
|
|
209
|
+
approved: z.ZodLiteral<false>;
|
|
210
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
211
|
+
}, z.core.$strip>;
|
|
212
|
+
}, z.core.$strip>], "state">>]>>;
|
|
213
|
+
createdAt: z.ZodPipe<z.ZodTransform<Date | undefined, unknown>, z.ZodOptional<z.ZodDate>>;
|
|
214
|
+
metadata: z.ZodOptional<z.ZodObject<{
|
|
215
|
+
parent_id: z.ZodNullable<z.ZodUUID>;
|
|
216
|
+
}, z.core.$strip>>;
|
|
217
|
+
}, z.core.$loose>;
|
|
218
|
+
type SendMessagePayload = {
|
|
219
|
+
parts: MessagePart[];
|
|
220
|
+
metadata?: MessageMetadata;
|
|
221
|
+
/** Optional vault items to attach to the message. */
|
|
222
|
+
vaultItems?: WsVaultItem[];
|
|
223
|
+
};
|
|
224
|
+
type SendMessageFn = (payload: SendMessagePayload) => Promise<void>;
|
|
225
|
+
type StreamDelta = WsStreamMessage["delta"];
|
|
226
|
+
type ChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
227
|
+
type ClarifyData = WsClarifyQuestion[];
|
|
228
|
+
|
|
229
|
+
declare class MessageNode {
|
|
230
|
+
readonly id: string;
|
|
231
|
+
readonly parent_id: string | null;
|
|
232
|
+
readonly message: ChatMessage;
|
|
233
|
+
private children;
|
|
234
|
+
constructor(message: ChatMessage);
|
|
235
|
+
addChild(child: MessageNode): void;
|
|
236
|
+
getChildren(): readonly MessageNode[];
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Builds a tree of MessageNodes from a flat list of messages using a two-pass approach.
|
|
240
|
+
* This ensures that children are connected even if the parent appears later in the array.
|
|
241
|
+
* The input messages SHOULD BE SORTED by `created_at` ASC for optimal performance.
|
|
242
|
+
* @param messages - An array of ChatMessage objects.
|
|
243
|
+
* @param byId - An empty Map that will be populated with all nodes by their ID.
|
|
244
|
+
* @returns An array of root MessageNode(s).
|
|
245
|
+
*/
|
|
246
|
+
declare function buildTree(messages: ChatMessage[], byId: Map<string, MessageNode>): MessageNode[];
|
|
247
|
+
/**
|
|
248
|
+
* Finds the latest path in the conversation tree by following
|
|
249
|
+
* the leaf with the most recent `createdAt` timestamp.
|
|
250
|
+
*/
|
|
251
|
+
declare function findLatestPath(tree: MessageNode[], byId: Map<string, MessageNode>): ChatMessage[];
|
|
252
|
+
|
|
253
|
+
type ConnectionState = "disconnected" | "connecting" | "connected";
|
|
254
|
+
type WebSocketConnectionOptions = {
|
|
255
|
+
/** WebSocket URL (can be relative, will be converted to ws/wss) */
|
|
256
|
+
url: string;
|
|
257
|
+
/** Called when a parsed message is received */
|
|
258
|
+
onMessage: (message: unknown) => void;
|
|
259
|
+
/** Called when connection state changes */
|
|
260
|
+
onConnectionChange?: (state: ConnectionState) => void;
|
|
261
|
+
/** Called when an error occurs */
|
|
262
|
+
onError?: (error: Event) => void;
|
|
263
|
+
/** Heartbeat interval in ms (default: 5000) */
|
|
264
|
+
heartbeatInterval?: number;
|
|
265
|
+
/** How long to wait for pong before considering connection dead (default: 10000) */
|
|
266
|
+
heartbeatTimeout?: number;
|
|
267
|
+
/** Initial reconnection delay in ms (default: 1000) */
|
|
268
|
+
initialReconnectDelay?: number;
|
|
269
|
+
/** Maximum reconnection delay in ms (default: 30000) */
|
|
270
|
+
maxReconnectDelay?: number;
|
|
271
|
+
/** Log prefix for debugging */
|
|
272
|
+
debugPrefix?: string;
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Framework-agnostic WebSocket connection with automatic reconnection and heartbeat.
|
|
276
|
+
*
|
|
277
|
+
* Handles:
|
|
278
|
+
* - Connection lifecycle (connect / disconnect / ensureConnected)
|
|
279
|
+
* - Reconnection with exponential backoff
|
|
280
|
+
* - Heartbeat ping/pong to detect stale connections
|
|
281
|
+
* - Message queue while disconnected
|
|
282
|
+
* - JSON serialization on send, JSON parsing on receive
|
|
283
|
+
*/
|
|
284
|
+
declare class WebSocketConnection {
|
|
285
|
+
private ws;
|
|
286
|
+
private reconnectTimeout;
|
|
287
|
+
private heartbeatIntervalTimer;
|
|
288
|
+
private heartbeatTimeoutTimer;
|
|
289
|
+
private reconnectDelay;
|
|
290
|
+
private pendingMessages;
|
|
291
|
+
private intentionalClose;
|
|
292
|
+
private awaitingPong;
|
|
293
|
+
private connectPromise;
|
|
294
|
+
private _state;
|
|
295
|
+
private readonly url;
|
|
296
|
+
private readonly heartbeatInterval;
|
|
297
|
+
private readonly heartbeatTimeout;
|
|
298
|
+
private readonly initialReconnectDelay;
|
|
299
|
+
private readonly maxReconnectDelay;
|
|
300
|
+
private readonly debugPrefix;
|
|
301
|
+
onMessage: (message: unknown) => void;
|
|
302
|
+
onConnectionChange?: (state: ConnectionState) => void;
|
|
303
|
+
onError?: (error: Event) => void;
|
|
304
|
+
get state(): ConnectionState;
|
|
305
|
+
get isConnected(): boolean;
|
|
306
|
+
constructor(options: WebSocketConnectionOptions);
|
|
307
|
+
private updateState;
|
|
308
|
+
private clearTimers;
|
|
309
|
+
private sendPing;
|
|
310
|
+
private startHeartbeat;
|
|
311
|
+
connect(): void;
|
|
312
|
+
disconnect(): void;
|
|
313
|
+
send(message: unknown): void;
|
|
314
|
+
ensureConnected(): Promise<boolean>;
|
|
315
|
+
reconnect(): void;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
type WebSocketOptions<TServerMessage> = {
|
|
319
|
+
/** WebSocket URL (can be relative, will be converted to ws/wss) */
|
|
320
|
+
url: string;
|
|
321
|
+
/** Called when a message is received */
|
|
322
|
+
onMessage: (message: TServerMessage) => void;
|
|
323
|
+
/** Called when connection state changes */
|
|
324
|
+
onConnectionChange?: (state: ConnectionState) => void;
|
|
325
|
+
/** Called when an error occurs */
|
|
326
|
+
onError?: (error: Event) => void;
|
|
327
|
+
/** Whether the WebSocket should be enabled (default: true) */
|
|
328
|
+
enabled?: boolean;
|
|
329
|
+
/** Heartbeat interval in ms (default: 5000 = 5s) */
|
|
330
|
+
heartbeatInterval?: number;
|
|
331
|
+
/** How long to wait for pong before considering connection dead (default: 10000 = 10s) */
|
|
332
|
+
heartbeatTimeout?: number;
|
|
333
|
+
/** Initial reconnection delay in ms (default: 1000) */
|
|
334
|
+
initialReconnectDelay?: number;
|
|
335
|
+
/** Maximum reconnection delay in ms (default: 30000) */
|
|
336
|
+
maxReconnectDelay?: number;
|
|
337
|
+
/** Log prefix for debugging */
|
|
338
|
+
debugPrefix?: string;
|
|
339
|
+
};
|
|
340
|
+
type WebSocketResult<TClientCommand> = {
|
|
341
|
+
/** Current connection state */
|
|
342
|
+
connectionState: ConnectionState;
|
|
343
|
+
/** Whether the WebSocket is currently connected */
|
|
344
|
+
isConnected: boolean;
|
|
345
|
+
/** Send a message, auto-reconnecting if needed */
|
|
346
|
+
send: (message: TClientCommand) => void;
|
|
347
|
+
/** Ensure connection before performing an action */
|
|
348
|
+
ensureConnected: () => Promise<boolean>;
|
|
349
|
+
/** Force reconnection */
|
|
350
|
+
reconnect: () => void;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* A React hook wrapping {@link WebSocketConnection} with lifecycle management.
|
|
354
|
+
*
|
|
355
|
+
* - Instantiates a `WebSocketConnection` in a ref
|
|
356
|
+
* - Manages connect/disconnect on mount/unmount and when `enabled` changes
|
|
357
|
+
* - Bridges connection events to React state and callback props
|
|
358
|
+
* - Exposes `send`, `ensureConnected`, `reconnect` with stable references
|
|
359
|
+
*/
|
|
360
|
+
declare function useReconnectingWebSocket<TServerMessage, TClientCommand>({ url, onMessage, onConnectionChange, onError, enabled, heartbeatInterval, heartbeatTimeout, initialReconnectDelay, maxReconnectDelay, debugPrefix, }: WebSocketOptions<TServerMessage>): WebSocketResult<TClientCommand>;
|
|
361
|
+
|
|
362
|
+
type TransformedHistory = {
|
|
363
|
+
messages: ChatMessage[];
|
|
364
|
+
tree: MessageNode[];
|
|
365
|
+
byId: Map<string, MessageNode>;
|
|
366
|
+
initialPath: ChatMessage[];
|
|
367
|
+
activeMessageId?: string;
|
|
368
|
+
};
|
|
369
|
+
type UseStreamChatOptions = {
|
|
370
|
+
/** The chat ID to connect to. */
|
|
371
|
+
chatId: string;
|
|
372
|
+
/** Full WebSocket URL for the chat endpoint (e.g. `wss://api.example.com/api/v2/chat/<id>`). */
|
|
373
|
+
wsUrl: string;
|
|
374
|
+
/** Called when a new assistant message starts streaming. */
|
|
375
|
+
onStart?: (id: string) => void;
|
|
376
|
+
/** Called on each stream delta. */
|
|
377
|
+
onDelta?: (id: string, delta: StreamDelta, consolidated: boolean) => void;
|
|
378
|
+
/** Called when a stream resumes with accumulated parts. */
|
|
379
|
+
onResume?: (id: string, parts: MessagePart[]) => void;
|
|
380
|
+
/** Called when streaming finishes. */
|
|
381
|
+
onFinish?: () => void;
|
|
382
|
+
/** Called on a protocol error. */
|
|
383
|
+
onError?: (error: {
|
|
384
|
+
error?: string;
|
|
385
|
+
message?: string;
|
|
386
|
+
}) => void;
|
|
387
|
+
/** Called when chat history is received from the server. */
|
|
388
|
+
onHistory?: (history: TransformedHistory) => void;
|
|
389
|
+
};
|
|
390
|
+
type UseStreamChatReturn = {
|
|
391
|
+
/** Current list of messages in the active branch. */
|
|
392
|
+
messages: ChatMessage[];
|
|
393
|
+
/** Replace the messages list (e.g. for branch switching). */
|
|
394
|
+
setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
|
|
395
|
+
/** Send a new user message. */
|
|
396
|
+
sendMessage: (payload: SendMessagePayload) => Promise<void>;
|
|
397
|
+
/** Inject a user message into an active stream. */
|
|
398
|
+
injectMessage: (text: string) => Promise<void>;
|
|
399
|
+
/** Current chat status. */
|
|
400
|
+
status: ChatStatus;
|
|
401
|
+
/** Stop the current generation. */
|
|
402
|
+
stop: () => Promise<void>;
|
|
403
|
+
/** Reset the chat state. */
|
|
404
|
+
reset: () => void;
|
|
405
|
+
/** Whether the WebSocket is connected. */
|
|
406
|
+
isConnected: boolean;
|
|
407
|
+
/** Pending clarification request, if any. */
|
|
408
|
+
pendingClarify: WsClarifyRequestMessage["data"] | null;
|
|
409
|
+
/** Submit answers to a clarification request. */
|
|
410
|
+
sendClarifyResponse: (answers: Record<string, string>) => void;
|
|
411
|
+
};
|
|
412
|
+
declare function useStreamChat({ chatId, wsUrl, onStart, onDelta, onResume, onFinish, onError, onHistory, }: UseStreamChatOptions): UseStreamChatReturn;
|
|
413
|
+
|
|
414
|
+
export { type ChatMessage, type ChatStatus, type ClarifyData, type ConnectionState, type MessageMetadata, MessageNode, type SendMessageFn, type SendMessagePayload, type StreamDelta, type UseStreamChatOptions, type UseStreamChatReturn, WebSocketConnection, type WebSocketConnectionOptions, type WebSocketOptions, type WebSocketResult, Z_ChatMessage, buildTree, findLatestPath, messageMetadataSchema, parseServerMessage, useReconnectingWebSocket, useStreamChat };
|