@beanx/cathygo-web-core 0.1.2 → 0.1.3
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/dist/index.d.ts +10 -0
- package/dist/index.js +251 -5
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ type ChatAttachment = {
|
|
|
73
73
|
width?: number | null;
|
|
74
74
|
height?: number | null;
|
|
75
75
|
created_at?: string | null;
|
|
76
|
+
thumbnail?: {
|
|
77
|
+
mime_type: string;
|
|
78
|
+
data_base64: string;
|
|
79
|
+
width?: number | null;
|
|
80
|
+
height?: number | null;
|
|
81
|
+
} | null;
|
|
76
82
|
};
|
|
77
83
|
type ChatMessagePart = {
|
|
78
84
|
type: 'text';
|
|
@@ -88,6 +94,8 @@ type ChatMessage$1 = {
|
|
|
88
94
|
content: string;
|
|
89
95
|
parts?: ChatMessagePart[];
|
|
90
96
|
turnId?: string;
|
|
97
|
+
clientInputId?: string;
|
|
98
|
+
clientTurnId?: string;
|
|
91
99
|
status?: 'streaming' | 'done' | 'error';
|
|
92
100
|
};
|
|
93
101
|
type ChatState = {
|
|
@@ -143,6 +151,8 @@ type ChatAction = {
|
|
|
143
151
|
id: string;
|
|
144
152
|
content: string;
|
|
145
153
|
parts?: ChatMessagePart[];
|
|
154
|
+
clientInputId?: string;
|
|
155
|
+
clientTurnId?: string;
|
|
146
156
|
} | {
|
|
147
157
|
type: 'session.input.accepted';
|
|
148
158
|
sessionId: string;
|
package/dist/index.js
CHANGED
|
@@ -1459,7 +1459,7 @@ function ChatMessage({ message }) {
|
|
|
1459
1459
|
"img",
|
|
1460
1460
|
{
|
|
1461
1461
|
alt: part.attachment.original_name ?? "Attached image",
|
|
1462
|
-
src: part.attachment
|
|
1462
|
+
src: attachmentImageSrc(part.attachment)
|
|
1463
1463
|
},
|
|
1464
1464
|
part.attachment_id
|
|
1465
1465
|
) : null
|
|
@@ -1481,6 +1481,18 @@ function ChatMessage({ message }) {
|
|
|
1481
1481
|
}
|
|
1482
1482
|
);
|
|
1483
1483
|
}
|
|
1484
|
+
function attachmentImageSrc(attachment) {
|
|
1485
|
+
if (isRenderableUri(attachment.uri)) return attachment.uri;
|
|
1486
|
+
const thumbnail = attachment.thumbnail;
|
|
1487
|
+
if (thumbnail?.mime_type && thumbnail.data_base64) {
|
|
1488
|
+
return `data:${thumbnail.mime_type};base64,${thumbnail.data_base64}`;
|
|
1489
|
+
}
|
|
1490
|
+
return attachment.uri;
|
|
1491
|
+
}
|
|
1492
|
+
function isRenderableUri(uri) {
|
|
1493
|
+
if (!uri) return false;
|
|
1494
|
+
return uri.startsWith("blob:") || uri.startsWith("data:") || uri.startsWith("http://") || uri.startsWith("https://");
|
|
1495
|
+
}
|
|
1484
1496
|
|
|
1485
1497
|
// src/chat-ui/components/ScrollToBottomButton.tsx
|
|
1486
1498
|
import { useEffect as useEffect3, useState as useState5 } from "react";
|
|
@@ -1877,11 +1889,12 @@ function reduceChat(state, action) {
|
|
|
1877
1889
|
if (action.type === "session.loaded") {
|
|
1878
1890
|
const runtime = action.runtime;
|
|
1879
1891
|
const activeTurnId = optionalString(runtime?.active_turn_id);
|
|
1892
|
+
const messages = reconcileLoadedMessages(state, action.sessionId, action.messages);
|
|
1880
1893
|
return {
|
|
1881
1894
|
...state,
|
|
1882
1895
|
sessionId: action.sessionId,
|
|
1883
1896
|
activeTurnId,
|
|
1884
|
-
messages
|
|
1897
|
+
messages,
|
|
1885
1898
|
activitiesByTurnId: groupActivitiesByTurn(action.activities ?? []),
|
|
1886
1899
|
progressByTurnId: activeTurnId ? setProgress({}, runtimeProgressFromSnapshot(runtime, activeTurnId)) : {},
|
|
1887
1900
|
runtime,
|
|
@@ -1911,6 +1924,8 @@ function reduceChat(state, action) {
|
|
|
1911
1924
|
role: "user",
|
|
1912
1925
|
content: action.content,
|
|
1913
1926
|
parts: action.parts,
|
|
1927
|
+
clientInputId: action.clientInputId,
|
|
1928
|
+
clientTurnId: action.clientTurnId,
|
|
1914
1929
|
status: "done"
|
|
1915
1930
|
}
|
|
1916
1931
|
],
|
|
@@ -1933,10 +1948,15 @@ function reduceChat(state, action) {
|
|
|
1933
1948
|
};
|
|
1934
1949
|
}
|
|
1935
1950
|
if (action.type === "session.input.accepted") {
|
|
1951
|
+
const messages = attachClientIdsToPendingUser(state.messages, {
|
|
1952
|
+
clientInputId: action.clientInputId,
|
|
1953
|
+
clientTurnId: action.clientTurnId
|
|
1954
|
+
});
|
|
1936
1955
|
return {
|
|
1937
1956
|
...state,
|
|
1938
1957
|
sessionId: action.sessionId,
|
|
1939
1958
|
activeTurnId: action.turnId,
|
|
1959
|
+
messages,
|
|
1940
1960
|
runtime: {
|
|
1941
1961
|
...state.runtime ?? emptyRuntimeSnapshot(),
|
|
1942
1962
|
status: "running",
|
|
@@ -1960,6 +1980,9 @@ function reduceChat(state, action) {
|
|
|
1960
1980
|
}
|
|
1961
1981
|
const event = action.event;
|
|
1962
1982
|
const payload = event.payload;
|
|
1983
|
+
if (isDuplicateEvent(state.runtime, event)) {
|
|
1984
|
+
return state;
|
|
1985
|
+
}
|
|
1963
1986
|
const next = {
|
|
1964
1987
|
...state,
|
|
1965
1988
|
eventCount: state.eventCount + 1,
|
|
@@ -2008,6 +2031,14 @@ function reduceChat(state, action) {
|
|
|
2008
2031
|
status: "Stopping"
|
|
2009
2032
|
};
|
|
2010
2033
|
}
|
|
2034
|
+
if (event.event === "user.message.created") {
|
|
2035
|
+
const message = userMessageFromEvent(event);
|
|
2036
|
+
if (!message) return next;
|
|
2037
|
+
return {
|
|
2038
|
+
...next,
|
|
2039
|
+
messages: dedupeMessages([...next.messages, message])
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2011
2042
|
if (event.event === "agent.progress.delta") {
|
|
2012
2043
|
return {
|
|
2013
2044
|
...next,
|
|
@@ -2073,7 +2104,8 @@ function reduceChat(state, action) {
|
|
|
2073
2104
|
next.messages,
|
|
2074
2105
|
payload.message_id,
|
|
2075
2106
|
finalMessage,
|
|
2076
|
-
payload.turn_id
|
|
2107
|
+
payload.turn_id,
|
|
2108
|
+
optionalString(payload.persisted_message_id)
|
|
2077
2109
|
)
|
|
2078
2110
|
};
|
|
2079
2111
|
}
|
|
@@ -2137,20 +2169,34 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
|
|
|
2137
2169
|
} : message
|
|
2138
2170
|
);
|
|
2139
2171
|
}
|
|
2140
|
-
function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
|
|
2172
|
+
function finalizeAssistantMessage(messages, messageId, finalMessage, turnId, persistedMessageId) {
|
|
2141
2173
|
const id = String(messageId || "");
|
|
2142
2174
|
const resolvedTurnId = optionalString(turnId);
|
|
2175
|
+
const existingTurnAssistantIndex = resolvedTurnId ? messages.findIndex(
|
|
2176
|
+
(message) => message.role === "assistant" && message.turnId === resolvedTurnId && message.status === "done" && Boolean(message.content)
|
|
2177
|
+
) : -1;
|
|
2143
2178
|
if (!id && !finalMessage) {
|
|
2144
2179
|
return messages.map(
|
|
2145
2180
|
(message) => message.role === "assistant" && message.status === "streaming" ? { ...message, turnId: message.turnId ?? resolvedTurnId, status: "done" } : message
|
|
2146
2181
|
);
|
|
2147
2182
|
}
|
|
2148
2183
|
const index = messages.findIndex((message) => message.id === id);
|
|
2184
|
+
if (index === -1 && existingTurnAssistantIndex !== -1) {
|
|
2185
|
+
return messages.map(
|
|
2186
|
+
(message, itemIndex) => itemIndex === existingTurnAssistantIndex ? {
|
|
2187
|
+
...message,
|
|
2188
|
+
id: persistedMessageId ?? (id || message.id),
|
|
2189
|
+
content: finalMessage || message.content,
|
|
2190
|
+
turnId: message.turnId ?? resolvedTurnId,
|
|
2191
|
+
status: "done"
|
|
2192
|
+
} : message
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2149
2195
|
if (index === -1 && finalMessage) {
|
|
2150
2196
|
return [
|
|
2151
2197
|
...messages,
|
|
2152
2198
|
{
|
|
2153
|
-
id: id || `msg_done_${messages.length}
|
|
2199
|
+
id: persistedMessageId ?? (id || `msg_done_${messages.length}`),
|
|
2154
2200
|
role: "assistant",
|
|
2155
2201
|
content: finalMessage,
|
|
2156
2202
|
turnId: resolvedTurnId,
|
|
@@ -2161,12 +2207,212 @@ function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
|
|
|
2161
2207
|
return messages.map(
|
|
2162
2208
|
(message, itemIndex) => itemIndex === index || message.role === "assistant" && message.status === "streaming" ? {
|
|
2163
2209
|
...message,
|
|
2210
|
+
id: itemIndex === index && persistedMessageId && message.id === id ? persistedMessageId : message.id,
|
|
2164
2211
|
content: message.content || finalMessage,
|
|
2165
2212
|
turnId: message.turnId ?? resolvedTurnId,
|
|
2166
2213
|
status: "done"
|
|
2167
2214
|
} : message
|
|
2168
2215
|
);
|
|
2169
2216
|
}
|
|
2217
|
+
function reconcileLoadedMessages(state, sessionId, loadedMessages) {
|
|
2218
|
+
if (state.sessionId !== sessionId || state.messages.length === 0) {
|
|
2219
|
+
return dedupeMessages(loadedMessages);
|
|
2220
|
+
}
|
|
2221
|
+
const localMessagesByClientId = /* @__PURE__ */ new Map();
|
|
2222
|
+
for (const message of state.messages) {
|
|
2223
|
+
if (message.role !== "user") continue;
|
|
2224
|
+
for (const key of messageClientKeys(message)) {
|
|
2225
|
+
localMessagesByClientId.set(key, message);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const reconciled = loadedMessages.map((message) => {
|
|
2229
|
+
if (message.role !== "user") return message;
|
|
2230
|
+
const local = messageClientKeys(message).map((key) => localMessagesByClientId.get(key)).find((item) => Boolean(item));
|
|
2231
|
+
if (!local) return message;
|
|
2232
|
+
return mergeLoadedUserMessage(local, message);
|
|
2233
|
+
});
|
|
2234
|
+
const loadedClientKeys = new Set(reconciled.flatMap(messageClientKeys));
|
|
2235
|
+
const pendingLocalMessages = state.messages.filter((message) => {
|
|
2236
|
+
if (message.role !== "user" || message.status === "error") return false;
|
|
2237
|
+
const keys = messageClientKeys(message);
|
|
2238
|
+
return keys.length > 0 && keys.every((key) => !loadedClientKeys.has(key));
|
|
2239
|
+
});
|
|
2240
|
+
return dedupeMessages([...reconciled, ...pendingLocalMessages]);
|
|
2241
|
+
}
|
|
2242
|
+
function mergeLoadedUserMessage(local, loaded) {
|
|
2243
|
+
return {
|
|
2244
|
+
...loaded,
|
|
2245
|
+
content: loaded.content || local.content,
|
|
2246
|
+
parts: mergeMessageParts(local.parts, loaded.parts),
|
|
2247
|
+
clientInputId: loaded.clientInputId ?? local.clientInputId,
|
|
2248
|
+
clientTurnId: loaded.clientTurnId ?? local.clientTurnId,
|
|
2249
|
+
status: loaded.status ?? "done"
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function mergeMessageParts(localParts, loadedParts) {
|
|
2253
|
+
if (!loadedParts?.length) return localParts;
|
|
2254
|
+
if (!localParts?.length) return loadedParts;
|
|
2255
|
+
return loadedParts.map((part) => {
|
|
2256
|
+
if (part.type !== "image") return part;
|
|
2257
|
+
const local = localParts.find(
|
|
2258
|
+
(item) => item.type === "image" && item.attachment_id === part.attachment_id
|
|
2259
|
+
);
|
|
2260
|
+
if (!local) return part;
|
|
2261
|
+
if (isLocalAttachmentPreview(local.attachment.uri) && !isReachableAttachmentUri(part.attachment.uri)) {
|
|
2262
|
+
return {
|
|
2263
|
+
...part,
|
|
2264
|
+
attachment: {
|
|
2265
|
+
...part.attachment,
|
|
2266
|
+
uri: local.attachment.uri
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
return part;
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
function userMessageFromEvent(event) {
|
|
2274
|
+
const payload = event.payload;
|
|
2275
|
+
const messageId = optionalString(payload.message_id);
|
|
2276
|
+
if (!messageId) return void 0;
|
|
2277
|
+
const metadata = plainRecord(payload.metadata);
|
|
2278
|
+
return {
|
|
2279
|
+
id: messageId,
|
|
2280
|
+
role: "user",
|
|
2281
|
+
content: optionalString(payload.message) ?? "",
|
|
2282
|
+
parts: chatPartsFromPayload(payload.parts),
|
|
2283
|
+
turnId: optionalString(payload.turn_id),
|
|
2284
|
+
clientInputId: optionalString(payload.client_input_id) ?? metadataString(metadata, "client_input_id"),
|
|
2285
|
+
clientTurnId: optionalString(payload.client_turn_id) ?? metadataString(metadata, "client_turn_id"),
|
|
2286
|
+
status: "done"
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function chatPartsFromPayload(parts) {
|
|
2290
|
+
if (!Array.isArray(parts)) return void 0;
|
|
2291
|
+
const normalized = parts.map((part) => {
|
|
2292
|
+
const raw = plainRecord(part);
|
|
2293
|
+
if (!raw) return void 0;
|
|
2294
|
+
const type = optionalString(raw.type);
|
|
2295
|
+
if (type === "text") {
|
|
2296
|
+
const text = optionalString(raw.text);
|
|
2297
|
+
return text ? { type: "text", text } : void 0;
|
|
2298
|
+
}
|
|
2299
|
+
if (type === "image") {
|
|
2300
|
+
const attachmentId = optionalString(raw.attachment_id);
|
|
2301
|
+
const attachment = chatAttachmentFromPayload(raw.attachment, attachmentId);
|
|
2302
|
+
if (!attachmentId || !attachment) return void 0;
|
|
2303
|
+
return {
|
|
2304
|
+
type: "image",
|
|
2305
|
+
attachment_id: attachmentId,
|
|
2306
|
+
attachment
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
return void 0;
|
|
2310
|
+
}).filter((part) => Boolean(part));
|
|
2311
|
+
return normalized.length ? normalized : void 0;
|
|
2312
|
+
}
|
|
2313
|
+
function chatAttachmentFromPayload(value, attachmentId) {
|
|
2314
|
+
const raw = plainRecord(value);
|
|
2315
|
+
if (!raw && !attachmentId) return void 0;
|
|
2316
|
+
return {
|
|
2317
|
+
id: optionalString(raw?.id) ?? attachmentId ?? "",
|
|
2318
|
+
kind: "image",
|
|
2319
|
+
uri: optionalString(raw?.uri) ?? "",
|
|
2320
|
+
original_name: optionalString(raw?.original_name) ?? null,
|
|
2321
|
+
mime_type: optionalString(raw?.mime_type) ?? null,
|
|
2322
|
+
size_bytes: optionalNumber(raw?.size_bytes),
|
|
2323
|
+
sha256: optionalString(raw?.sha256) ?? null,
|
|
2324
|
+
width: optionalNumber(raw?.width),
|
|
2325
|
+
height: optionalNumber(raw?.height),
|
|
2326
|
+
created_at: optionalString(raw?.created_at) ?? null,
|
|
2327
|
+
thumbnail: thumbnailFromPayload(raw?.thumbnail)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
function thumbnailFromPayload(value) {
|
|
2331
|
+
const raw = plainRecord(value);
|
|
2332
|
+
if (!raw) return null;
|
|
2333
|
+
const mimeType = optionalString(raw.mime_type);
|
|
2334
|
+
const dataBase64 = optionalString(raw.data_base64);
|
|
2335
|
+
if (!mimeType || !dataBase64) return null;
|
|
2336
|
+
return {
|
|
2337
|
+
mime_type: mimeType,
|
|
2338
|
+
data_base64: dataBase64,
|
|
2339
|
+
width: optionalNumber(raw.width),
|
|
2340
|
+
height: optionalNumber(raw.height)
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function dedupeMessages(messages) {
|
|
2344
|
+
const next = [];
|
|
2345
|
+
for (const message of messages) {
|
|
2346
|
+
const duplicateIndex = next.findIndex((item) => messagesMatch(item, message));
|
|
2347
|
+
if (duplicateIndex === -1) {
|
|
2348
|
+
next.push(message);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
next[duplicateIndex] = mergeDuplicateMessage(next[duplicateIndex], message);
|
|
2352
|
+
}
|
|
2353
|
+
return next;
|
|
2354
|
+
}
|
|
2355
|
+
function messagesMatch(left, right) {
|
|
2356
|
+
if (left.id === right.id) return true;
|
|
2357
|
+
if (left.role !== right.role) return false;
|
|
2358
|
+
if (left.role === "assistant" && left.turnId && left.turnId === right.turnId) return true;
|
|
2359
|
+
const leftKeys = new Set(messageClientKeys(left));
|
|
2360
|
+
return messageClientKeys(right).some((key) => leftKeys.has(key));
|
|
2361
|
+
}
|
|
2362
|
+
function mergeDuplicateMessage(base, update) {
|
|
2363
|
+
const preferUpdate = base.id.startsWith("user_") && !update.id.startsWith("user_") || base.id.startsWith("msg_") && !update.id.startsWith("msg_") || update.status === "done";
|
|
2364
|
+
const primary = preferUpdate ? update : base;
|
|
2365
|
+
const secondary = preferUpdate ? base : update;
|
|
2366
|
+
return {
|
|
2367
|
+
...primary,
|
|
2368
|
+
content: primary.content || secondary.content,
|
|
2369
|
+
parts: mergeMessageParts(secondary.parts, primary.parts),
|
|
2370
|
+
turnId: primary.turnId ?? secondary.turnId,
|
|
2371
|
+
clientInputId: primary.clientInputId ?? secondary.clientInputId,
|
|
2372
|
+
clientTurnId: primary.clientTurnId ?? secondary.clientTurnId,
|
|
2373
|
+
status: primary.status ?? secondary.status
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
function attachClientIdsToPendingUser(messages, ids) {
|
|
2377
|
+
if (!ids.clientInputId && !ids.clientTurnId) return messages;
|
|
2378
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
2379
|
+
const message = messages[index];
|
|
2380
|
+
if (message.role !== "user") continue;
|
|
2381
|
+
if (message.clientInputId || message.clientTurnId) return messages;
|
|
2382
|
+
return messages.map(
|
|
2383
|
+
(item, itemIndex) => itemIndex === index ? {
|
|
2384
|
+
...item,
|
|
2385
|
+
clientInputId: ids.clientInputId,
|
|
2386
|
+
clientTurnId: ids.clientTurnId
|
|
2387
|
+
} : item
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
return messages;
|
|
2391
|
+
}
|
|
2392
|
+
function messageClientKeys(message) {
|
|
2393
|
+
return [message.clientInputId, message.clientTurnId].map((value) => optionalString(value)).filter((value) => Boolean(value));
|
|
2394
|
+
}
|
|
2395
|
+
function metadataString(metadata, key) {
|
|
2396
|
+
return optionalString(metadata?.[key]);
|
|
2397
|
+
}
|
|
2398
|
+
function isDuplicateEvent(runtime, event) {
|
|
2399
|
+
if (!event.seq || event.seq <= 0) return false;
|
|
2400
|
+
return event.seq <= (runtime?.last_event_seq ?? 0);
|
|
2401
|
+
}
|
|
2402
|
+
function plainRecord(value) {
|
|
2403
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2404
|
+
return value;
|
|
2405
|
+
}
|
|
2406
|
+
function optionalNumber(value) {
|
|
2407
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2408
|
+
}
|
|
2409
|
+
function isLocalAttachmentPreview(uri) {
|
|
2410
|
+
return Boolean(uri?.startsWith("blob:") || uri?.startsWith("data:"));
|
|
2411
|
+
}
|
|
2412
|
+
function isReachableAttachmentUri(uri) {
|
|
2413
|
+
if (!uri) return false;
|
|
2414
|
+
return uri.startsWith("blob:") || uri.startsWith("data:") || uri.startsWith("http://") || uri.startsWith("https://");
|
|
2415
|
+
}
|
|
2170
2416
|
function isAgentActivityEvent(event) {
|
|
2171
2417
|
return event.event.startsWith("agent.activity.");
|
|
2172
2418
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beanx/cathygo-web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./styles.css": "./dist/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@beanx/cathygo-protocol": "0.1.
|
|
19
|
+
"@beanx/cathygo-protocol": "0.1.2",
|
|
20
20
|
"@streamdown/math": "^1.0.2",
|
|
21
21
|
"katex": "^0.16.47",
|
|
22
22
|
"streamdown": "^2.5.0"
|