@futurity/chat-react 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -53
- package/dist/index.d.ts +80 -6
- package/dist/index.js +426 -12
- package/package.json +7 -3
- package/src/index.ts +7 -0
- package/src/stream-accumulator.ts +448 -0
- package/src/types.ts +3 -0
- package/src/useStreamChat.ts +135 -23
package/dist/index.js
CHANGED
|
@@ -13,6 +13,325 @@ function parseServerMessage(data) {
|
|
|
13
13
|
return result.data;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// src/stream-accumulator.ts
|
|
17
|
+
import {
|
|
18
|
+
Z_DataEndSplitSchema,
|
|
19
|
+
Z_DataSplitSchema,
|
|
20
|
+
Z_DataSubagentPartSchema
|
|
21
|
+
} from "@futurity/chat-protocol";
|
|
22
|
+
function isTypedChunk(x) {
|
|
23
|
+
return typeof x === "object" && x !== null && "type" in x && typeof x.type === "string";
|
|
24
|
+
}
|
|
25
|
+
var StreamAccumulator = class {
|
|
26
|
+
parts = [];
|
|
27
|
+
textById = /* @__PURE__ */ new Map();
|
|
28
|
+
reasoningById = /* @__PURE__ */ new Map();
|
|
29
|
+
toolById = /* @__PURE__ */ new Map();
|
|
30
|
+
partialToolText = /* @__PURE__ */ new Map();
|
|
31
|
+
processedCount = 0;
|
|
32
|
+
push(part) {
|
|
33
|
+
this.parts.push(part);
|
|
34
|
+
}
|
|
35
|
+
/** Process any new raw chunks and return the full accumulated parts array. */
|
|
36
|
+
accumulate(rawParts) {
|
|
37
|
+
for (let i = this.processedCount; i < rawParts.length; i++) {
|
|
38
|
+
const chunk = rawParts[i];
|
|
39
|
+
if (!isTypedChunk(chunk)) {
|
|
40
|
+
this.parts.push(chunk);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
switch (chunk.type) {
|
|
44
|
+
// --- Text accumulation ---
|
|
45
|
+
case "text-start": {
|
|
46
|
+
const part = { type: "text", text: "", state: "streaming" };
|
|
47
|
+
this.textById.set(chunk.id, part);
|
|
48
|
+
this.push(part);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "text-delta": {
|
|
52
|
+
const part = this.textById.get(chunk.id);
|
|
53
|
+
if (part) part.text += chunk.delta;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "text-end": {
|
|
57
|
+
const part = this.textById.get(chunk.id);
|
|
58
|
+
if (part) part.state = "done";
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// --- Reasoning accumulation ---
|
|
62
|
+
case "reasoning-start": {
|
|
63
|
+
const part = { type: "reasoning", text: "", state: "streaming" };
|
|
64
|
+
this.reasoningById.set(chunk.id, part);
|
|
65
|
+
this.push(part);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "reasoning-delta": {
|
|
69
|
+
const part = this.reasoningById.get(chunk.id);
|
|
70
|
+
if (part) part.text += chunk.delta;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "reasoning-end": {
|
|
74
|
+
const part = this.reasoningById.get(chunk.id);
|
|
75
|
+
if (part) part.state = "done";
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
// --- Tool invocation accumulation ---
|
|
79
|
+
case "tool-input-start": {
|
|
80
|
+
const typeName = chunk.dynamic ? "dynamic-tool" : `tool-${chunk.toolName}`;
|
|
81
|
+
const part = {
|
|
82
|
+
type: typeName,
|
|
83
|
+
toolCallId: chunk.toolCallId,
|
|
84
|
+
toolName: chunk.toolName,
|
|
85
|
+
state: "input-streaming",
|
|
86
|
+
input: void 0,
|
|
87
|
+
providerExecuted: chunk.providerExecuted,
|
|
88
|
+
title: chunk.title
|
|
89
|
+
};
|
|
90
|
+
this.toolById.set(chunk.toolCallId, part);
|
|
91
|
+
this.partialToolText.set(chunk.toolCallId, "");
|
|
92
|
+
this.push(part);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "tool-input-delta": {
|
|
96
|
+
const text = (this.partialToolText.get(chunk.toolCallId) ?? "") + (chunk.inputTextDelta ?? "");
|
|
97
|
+
this.partialToolText.set(chunk.toolCallId, text);
|
|
98
|
+
const part = this.toolById.get(chunk.toolCallId);
|
|
99
|
+
if (part) {
|
|
100
|
+
try {
|
|
101
|
+
part.input = JSON.parse(text);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "tool-input-available": {
|
|
108
|
+
const typeName = chunk.dynamic ? "dynamic-tool" : `tool-${chunk.toolName}`;
|
|
109
|
+
let part = this.toolById.get(chunk.toolCallId);
|
|
110
|
+
if (part) {
|
|
111
|
+
part.state = "input-available";
|
|
112
|
+
part.input = chunk.input;
|
|
113
|
+
part.providerExecuted = chunk.providerExecuted;
|
|
114
|
+
} else {
|
|
115
|
+
part = {
|
|
116
|
+
type: typeName,
|
|
117
|
+
toolCallId: chunk.toolCallId,
|
|
118
|
+
toolName: chunk.toolName,
|
|
119
|
+
state: "input-available",
|
|
120
|
+
input: chunk.input,
|
|
121
|
+
providerExecuted: chunk.providerExecuted,
|
|
122
|
+
title: chunk.title
|
|
123
|
+
};
|
|
124
|
+
this.toolById.set(chunk.toolCallId, part);
|
|
125
|
+
this.push(part);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "tool-input-error": {
|
|
130
|
+
const part = this.toolById.get(chunk.toolCallId);
|
|
131
|
+
if (part) {
|
|
132
|
+
part.state = "output-error";
|
|
133
|
+
part.errorText = chunk.errorText;
|
|
134
|
+
part.input = chunk.input;
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "tool-output-available": {
|
|
139
|
+
const part = this.toolById.get(chunk.toolCallId);
|
|
140
|
+
if (part) {
|
|
141
|
+
part.state = "output-available";
|
|
142
|
+
part.output = chunk.output;
|
|
143
|
+
part.preliminary = chunk.preliminary;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "tool-output-error": {
|
|
148
|
+
const part = this.toolById.get(chunk.toolCallId);
|
|
149
|
+
if (part) {
|
|
150
|
+
part.state = "output-error";
|
|
151
|
+
part.errorText = chunk.errorText;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
// --- Pass-through parts ---
|
|
156
|
+
case "file":
|
|
157
|
+
this.push({
|
|
158
|
+
type: "file",
|
|
159
|
+
mediaType: chunk.mediaType,
|
|
160
|
+
url: chunk.url,
|
|
161
|
+
filename: chunk.filename
|
|
162
|
+
});
|
|
163
|
+
break;
|
|
164
|
+
case "source-url":
|
|
165
|
+
this.push({
|
|
166
|
+
type: "source-url",
|
|
167
|
+
sourceId: chunk.sourceId,
|
|
168
|
+
url: chunk.url,
|
|
169
|
+
title: chunk.title
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
case "source-document":
|
|
173
|
+
this.push({
|
|
174
|
+
type: "source-document",
|
|
175
|
+
sourceId: chunk.sourceId,
|
|
176
|
+
mediaType: chunk.mediaType,
|
|
177
|
+
title: chunk.title,
|
|
178
|
+
filename: chunk.filename
|
|
179
|
+
});
|
|
180
|
+
break;
|
|
181
|
+
case "start-step":
|
|
182
|
+
this.push({ type: "step-start" });
|
|
183
|
+
break;
|
|
184
|
+
// --- Ignored protocol chunks ---
|
|
185
|
+
case "finish-step":
|
|
186
|
+
case "start":
|
|
187
|
+
case "finish":
|
|
188
|
+
case "error":
|
|
189
|
+
break;
|
|
190
|
+
// --- Unknown / custom data parts → pass through ---
|
|
191
|
+
default:
|
|
192
|
+
if (chunk.type.startsWith("data-")) {
|
|
193
|
+
this.push(chunk);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.processedCount = rawParts.length;
|
|
199
|
+
return this.parts;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function createPreprocessorState(messageId) {
|
|
203
|
+
return {
|
|
204
|
+
messageId: messageId ?? null,
|
|
205
|
+
scannedLength: 0,
|
|
206
|
+
claimedIndices: /* @__PURE__ */ new Set(),
|
|
207
|
+
openGroups: /* @__PURE__ */ new Map(),
|
|
208
|
+
groupsByStartIndex: /* @__PURE__ */ new Map(),
|
|
209
|
+
groupBySubAgentId: /* @__PURE__ */ new Map(),
|
|
210
|
+
accumulators: /* @__PURE__ */ new Map()
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function incrementalPreprocess(state, parts) {
|
|
214
|
+
for (let i2 = state.scannedLength; i2 < parts.length; i2++) {
|
|
215
|
+
const part = parts[i2];
|
|
216
|
+
if (!isTypedChunk(part)) continue;
|
|
217
|
+
if (part.type === "data-split") {
|
|
218
|
+
const splitParse = Z_DataSplitSchema.safeParse(part);
|
|
219
|
+
if (splitParse.success && splitParse.data.data.subAgentId) {
|
|
220
|
+
const sid = splitParse.data.data.subAgentId;
|
|
221
|
+
const group = {
|
|
222
|
+
startIndex: i2,
|
|
223
|
+
title: splitParse.data.data.title,
|
|
224
|
+
subtitle: splitParse.data.data.subtitle,
|
|
225
|
+
desktopSessionId: splitParse.data.data.desktopSessionId,
|
|
226
|
+
subAgentId: sid,
|
|
227
|
+
innerParts: [],
|
|
228
|
+
endIndex: null
|
|
229
|
+
};
|
|
230
|
+
state.openGroups.set(sid, group);
|
|
231
|
+
state.groupsByStartIndex.set(i2, group);
|
|
232
|
+
state.groupBySubAgentId.set(sid, group);
|
|
233
|
+
state.claimedIndices.add(i2);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (part.type === "data-endsplit") {
|
|
238
|
+
const endSplitParse = Z_DataEndSplitSchema.safeParse(part);
|
|
239
|
+
if (endSplitParse.success && endSplitParse.data?.data?.subAgentId) {
|
|
240
|
+
const sid = endSplitParse.data.data.subAgentId;
|
|
241
|
+
const group = state.openGroups.get(sid);
|
|
242
|
+
if (group) {
|
|
243
|
+
group.endIndex = i2;
|
|
244
|
+
group.desktopSessionId = group.desktopSessionId ?? endSplitParse.data.data.desktopSessionId;
|
|
245
|
+
state.openGroups.delete(sid);
|
|
246
|
+
}
|
|
247
|
+
state.claimedIndices.add(i2);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (part.type === "data-subagent-part") {
|
|
252
|
+
const subagentParse = Z_DataSubagentPartSchema.safeParse(part);
|
|
253
|
+
if (subagentParse.success) {
|
|
254
|
+
const sid = subagentParse.data.data.subAgentId;
|
|
255
|
+
const group = state.openGroups.get(sid) ?? state.groupBySubAgentId.get(sid);
|
|
256
|
+
if (group) {
|
|
257
|
+
group.innerParts.push(subagentParse.data.data.part);
|
|
258
|
+
}
|
|
259
|
+
state.claimedIndices.add(i2);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
state.scannedLength = parts.length;
|
|
264
|
+
const processed = [];
|
|
265
|
+
let i = 0;
|
|
266
|
+
while (i < parts.length) {
|
|
267
|
+
const groupAtIndex = state.groupsByStartIndex.get(i);
|
|
268
|
+
if (groupAtIndex) {
|
|
269
|
+
let acc = state.accumulators.get(groupAtIndex.startIndex);
|
|
270
|
+
if (!acc) {
|
|
271
|
+
acc = new StreamAccumulator();
|
|
272
|
+
state.accumulators.set(groupAtIndex.startIndex, acc);
|
|
273
|
+
}
|
|
274
|
+
processed.push({
|
|
275
|
+
type: "split-group",
|
|
276
|
+
title: groupAtIndex.title,
|
|
277
|
+
subtitle: groupAtIndex.subtitle,
|
|
278
|
+
// Incrementally accumulate — only processes newly added innerParts
|
|
279
|
+
parts: acc.accumulate(groupAtIndex.innerParts),
|
|
280
|
+
startIndex: groupAtIndex.startIndex,
|
|
281
|
+
endIndex: groupAtIndex.endIndex ?? parts.length,
|
|
282
|
+
desktopSessionId: groupAtIndex.desktopSessionId
|
|
283
|
+
});
|
|
284
|
+
i++;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (state.claimedIndices.has(i)) {
|
|
288
|
+
i++;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const part = parts[i];
|
|
292
|
+
const type = isTypedChunk(part) ? part.type : void 0;
|
|
293
|
+
if (type === "data-split") {
|
|
294
|
+
const splitParse = Z_DataSplitSchema.safeParse(part);
|
|
295
|
+
if (splitParse.success && !splitParse.data.data.subAgentId) {
|
|
296
|
+
let endIndex = parts.length;
|
|
297
|
+
let desktopSessionId = splitParse.data.data.desktopSessionId;
|
|
298
|
+
for (let searchIndex = i + 1; searchIndex < parts.length; searchIndex++) {
|
|
299
|
+
if (state.claimedIndices.has(searchIndex)) continue;
|
|
300
|
+
const checkPart = parts[searchIndex];
|
|
301
|
+
const endParse = Z_DataEndSplitSchema.safeParse(checkPart);
|
|
302
|
+
if (endParse.success && !endParse.data?.data?.subAgentId) {
|
|
303
|
+
endIndex = searchIndex;
|
|
304
|
+
desktopSessionId = desktopSessionId ?? endParse.data?.data?.desktopSessionId;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const groupedParts = parts.slice(i + 1, endIndex).filter((_, idx) => {
|
|
309
|
+
const absIdx = i + 1 + idx;
|
|
310
|
+
return !state.claimedIndices.has(absIdx);
|
|
311
|
+
});
|
|
312
|
+
processed.push({
|
|
313
|
+
type: "split-group",
|
|
314
|
+
title: splitParse.data.data.title,
|
|
315
|
+
subtitle: splitParse.data.data.subtitle,
|
|
316
|
+
parts: groupedParts,
|
|
317
|
+
startIndex: i,
|
|
318
|
+
endIndex,
|
|
319
|
+
desktopSessionId
|
|
320
|
+
});
|
|
321
|
+
i = endIndex < parts.length ? endIndex + 1 : endIndex;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (type === "data-endsplit") {
|
|
326
|
+
i++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
processed.push({ type: "regular", part, originalIndex: i });
|
|
330
|
+
i++;
|
|
331
|
+
}
|
|
332
|
+
return processed;
|
|
333
|
+
}
|
|
334
|
+
|
|
16
335
|
// src/tree-builder.ts
|
|
17
336
|
var MessageNode = class {
|
|
18
337
|
id;
|
|
@@ -379,12 +698,35 @@ function useReconnectingWebSocket({
|
|
|
379
698
|
|
|
380
699
|
// src/useStreamChat.ts
|
|
381
700
|
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
382
|
-
function
|
|
701
|
+
function simpleProcessedParts(parts) {
|
|
702
|
+
return parts.map((part, i) => ({
|
|
703
|
+
type: "regular",
|
|
704
|
+
part,
|
|
705
|
+
originalIndex: i
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
function preprocessMessage(msg, statesMap) {
|
|
709
|
+
if (msg.role !== "assistant") {
|
|
710
|
+
return { ...msg, processedParts: simpleProcessedParts(msg.parts) };
|
|
711
|
+
}
|
|
712
|
+
let state = statesMap.get(msg.id);
|
|
713
|
+
if (!state || state.scannedLength > (msg.parts?.length ?? 0)) {
|
|
714
|
+
state = createPreprocessorState(msg.id);
|
|
715
|
+
statesMap.set(msg.id, state);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
...msg,
|
|
719
|
+
processedParts: incrementalPreprocess(state, msg.parts ?? [])
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function transformChatHistory(rawMessages, statesMap) {
|
|
383
723
|
const parsedMessages = Z_ChatMessage.array().safeParse(rawMessages);
|
|
384
724
|
if (!parsedMessages.success) {
|
|
385
725
|
throw new Error("Invalid chat history");
|
|
386
726
|
}
|
|
387
|
-
const messages = parsedMessages.data
|
|
727
|
+
const messages = parsedMessages.data.map(
|
|
728
|
+
(msg) => preprocessMessage(msg, statesMap)
|
|
729
|
+
);
|
|
388
730
|
const byId = /* @__PURE__ */ new Map();
|
|
389
731
|
const tree = buildTree(messages, byId);
|
|
390
732
|
const initialPath = findLatestPath(tree, byId);
|
|
@@ -399,8 +741,6 @@ function useStreamChat({
|
|
|
399
741
|
chatId,
|
|
400
742
|
wsUrl,
|
|
401
743
|
onStart,
|
|
402
|
-
onDelta,
|
|
403
|
-
onResume,
|
|
404
744
|
onFinish,
|
|
405
745
|
onError,
|
|
406
746
|
onHistory
|
|
@@ -416,9 +756,8 @@ function useStreamChat({
|
|
|
416
756
|
const [job, setJob] = useState2("");
|
|
417
757
|
const hasRequestedChat = useRef2(false);
|
|
418
758
|
const [pendingClarify, setPendingClarify] = useState2(null);
|
|
759
|
+
const preprocessorStatesRef = useRef2(/* @__PURE__ */ new Map());
|
|
419
760
|
const onStartRef = useRef2(onStart);
|
|
420
|
-
const onDeltaRef = useRef2(onDelta);
|
|
421
|
-
const onResumeRef = useRef2(onResume);
|
|
422
761
|
const onFinishRef = useRef2(onFinish);
|
|
423
762
|
const onErrorRef = useRef2(onError);
|
|
424
763
|
const onHistoryRef = useRef2(onHistory);
|
|
@@ -426,8 +765,6 @@ function useStreamChat({
|
|
|
426
765
|
});
|
|
427
766
|
useEffect2(() => {
|
|
428
767
|
onStartRef.current = onStart;
|
|
429
|
-
onDeltaRef.current = onDelta;
|
|
430
|
-
onResumeRef.current = onResume;
|
|
431
768
|
onFinishRef.current = onFinish;
|
|
432
769
|
onErrorRef.current = onError;
|
|
433
770
|
onHistoryRef.current = onHistory;
|
|
@@ -459,14 +796,81 @@ function useStreamChat({
|
|
|
459
796
|
}
|
|
460
797
|
case "stream": {
|
|
461
798
|
const messageId = message.messageId;
|
|
799
|
+
const delta = message.delta;
|
|
800
|
+
const consolidated = message.consolidated;
|
|
462
801
|
setStatus("streaming");
|
|
463
|
-
|
|
802
|
+
setMessages((prev) => {
|
|
803
|
+
const states = preprocessorStatesRef.current;
|
|
804
|
+
const exists = prev.some((m) => m.id === messageId);
|
|
805
|
+
let updated;
|
|
806
|
+
if (!exists) {
|
|
807
|
+
const newMsg = {
|
|
808
|
+
id: messageId,
|
|
809
|
+
role: "assistant",
|
|
810
|
+
parts: [delta],
|
|
811
|
+
processedParts: []
|
|
812
|
+
};
|
|
813
|
+
updated = [...prev, newMsg];
|
|
814
|
+
} else {
|
|
815
|
+
updated = prev.map((m) => {
|
|
816
|
+
if (m.id !== messageId) return m;
|
|
817
|
+
const parts = m.parts ?? [];
|
|
818
|
+
const nextParts = consolidated ? [...parts.slice(0, -1), delta] : [...parts, delta];
|
|
819
|
+
return { ...m, parts: nextParts };
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return updated.map(
|
|
823
|
+
(m) => m.id === messageId ? preprocessMessage(m, states) : m
|
|
824
|
+
);
|
|
825
|
+
});
|
|
464
826
|
break;
|
|
465
827
|
}
|
|
466
828
|
case "stream_resume": {
|
|
467
829
|
const messageId = message.messageId;
|
|
468
830
|
setStatus("streaming");
|
|
469
|
-
|
|
831
|
+
setMessages((prev) => {
|
|
832
|
+
const states = preprocessorStatesRef.current;
|
|
833
|
+
states.delete(messageId);
|
|
834
|
+
return prev.map((m) => {
|
|
835
|
+
if (m.id !== messageId) return m;
|
|
836
|
+
const updated = { ...m, parts: message.parts };
|
|
837
|
+
return preprocessMessage(updated, states);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
case "inject_ack": {
|
|
843
|
+
const { new_assistant_id } = message.data;
|
|
844
|
+
setMessages((prev) => {
|
|
845
|
+
const exists = prev.some((m) => m.id === new_assistant_id);
|
|
846
|
+
if (exists) return prev;
|
|
847
|
+
return [
|
|
848
|
+
...prev,
|
|
849
|
+
{
|
|
850
|
+
id: new_assistant_id,
|
|
851
|
+
role: "assistant",
|
|
852
|
+
parts: [],
|
|
853
|
+
processedParts: []
|
|
854
|
+
}
|
|
855
|
+
];
|
|
856
|
+
});
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
case "inject_split": {
|
|
860
|
+
const { new_message_id } = message.data;
|
|
861
|
+
setMessages((prev) => {
|
|
862
|
+
const exists = prev.some((m) => m.id === new_message_id);
|
|
863
|
+
if (exists) return prev;
|
|
864
|
+
return [
|
|
865
|
+
...prev,
|
|
866
|
+
{
|
|
867
|
+
id: new_message_id,
|
|
868
|
+
role: "assistant",
|
|
869
|
+
parts: [],
|
|
870
|
+
processedParts: []
|
|
871
|
+
}
|
|
872
|
+
];
|
|
873
|
+
});
|
|
470
874
|
break;
|
|
471
875
|
}
|
|
472
876
|
case "done": {
|
|
@@ -481,7 +885,10 @@ function useStreamChat({
|
|
|
481
885
|
break;
|
|
482
886
|
}
|
|
483
887
|
case "chat_history": {
|
|
484
|
-
const result = transformChatHistory(
|
|
888
|
+
const result = transformChatHistory(
|
|
889
|
+
message.data.messages,
|
|
890
|
+
preprocessorStatesRef.current
|
|
891
|
+
);
|
|
485
892
|
result.activeMessageId = message.data.activeMessageId;
|
|
486
893
|
onHistoryRef.current?.(result);
|
|
487
894
|
break;
|
|
@@ -535,6 +942,7 @@ function useStreamChat({
|
|
|
535
942
|
id: crypto.randomUUID(),
|
|
536
943
|
role: "user",
|
|
537
944
|
parts: payload.parts,
|
|
945
|
+
processedParts: simpleProcessedParts(payload.parts),
|
|
538
946
|
metadata: {
|
|
539
947
|
parent_id
|
|
540
948
|
}
|
|
@@ -570,12 +978,14 @@ function useStreamChat({
|
|
|
570
978
|
const connected = await ensureConnected();
|
|
571
979
|
if (!connected) return;
|
|
572
980
|
const message_id = crypto.randomUUID();
|
|
981
|
+
const parts = [{ type: "text", text }];
|
|
573
982
|
setMessages((prev) => [
|
|
574
983
|
...prev,
|
|
575
984
|
{
|
|
576
985
|
id: message_id,
|
|
577
986
|
role: "user",
|
|
578
|
-
parts
|
|
987
|
+
parts,
|
|
988
|
+
processedParts: simpleProcessedParts(parts)
|
|
579
989
|
}
|
|
580
990
|
]);
|
|
581
991
|
send({
|
|
@@ -610,6 +1020,7 @@ function useStreamChat({
|
|
|
610
1020
|
setJob("");
|
|
611
1021
|
setPendingClarify(null);
|
|
612
1022
|
hasRequestedChat.current = false;
|
|
1023
|
+
preprocessorStatesRef.current.clear();
|
|
613
1024
|
}, []);
|
|
614
1025
|
return {
|
|
615
1026
|
messages,
|
|
@@ -626,10 +1037,13 @@ function useStreamChat({
|
|
|
626
1037
|
}
|
|
627
1038
|
export {
|
|
628
1039
|
MessageNode,
|
|
1040
|
+
StreamAccumulator,
|
|
629
1041
|
WebSocketConnection,
|
|
630
1042
|
Z_ChatMessage,
|
|
631
1043
|
buildTree,
|
|
1044
|
+
createPreprocessorState,
|
|
632
1045
|
findLatestPath,
|
|
1046
|
+
incrementalPreprocess,
|
|
633
1047
|
messageMetadataSchema,
|
|
634
1048
|
parseServerMessage,
|
|
635
1049
|
useReconnectingWebSocket,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futurity/chat-react",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
},
|
|
14
14
|
"main": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
|
-
"files": [
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
17
21
|
"scripts": {
|
|
18
22
|
"build": "tsup",
|
|
19
23
|
"check": "tsgo --noEmit",
|
|
20
|
-
"prepublishOnly": "tsup"
|
|
24
|
+
"prepublishOnly": "bunx tsup"
|
|
21
25
|
},
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"@futurity/chat-protocol": "0.1.0"
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
export type { ClientCommand } from "./chat-protocol";
|
|
5
5
|
// Protocol helpers
|
|
6
6
|
export { parseServerMessage } from "./chat-protocol";
|
|
7
|
+
// Stream accumulator
|
|
8
|
+
export type { PreprocessorState, ProcessedPart } from "./stream-accumulator";
|
|
9
|
+
export {
|
|
10
|
+
createPreprocessorState,
|
|
11
|
+
incrementalPreprocess,
|
|
12
|
+
StreamAccumulator,
|
|
13
|
+
} from "./stream-accumulator";
|
|
7
14
|
// Tree utilities
|
|
8
15
|
export { buildTree, findLatestPath, MessageNode } from "./tree-builder";
|
|
9
16
|
// Types
|