@agents24/chat-react 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 +18 -0
- package/dist/index.cjs +1306 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +351 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +1260 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
// src/controller.ts
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/model.ts
|
|
5
|
+
var DEFAULT_THREAD_PAGE_SIZE = 5;
|
|
6
|
+
var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running"])).has(String(status || "").toLowerCase());
|
|
7
|
+
var createChatId = () => {
|
|
8
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
|
|
9
|
+
return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
};
|
|
11
|
+
var titleFromMessage = (text, files = []) => {
|
|
12
|
+
const base = text.trim() || files[0]?.filename || "New chat";
|
|
13
|
+
return base.replace(/\s+/g, " ").slice(0, 48);
|
|
14
|
+
};
|
|
15
|
+
var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
|
|
16
|
+
var assistantTextFromBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
|
|
17
|
+
var textFromFinalOutput = (value) => {
|
|
18
|
+
if (typeof value === "string") return value;
|
|
19
|
+
if (!value || typeof value !== "object") return "";
|
|
20
|
+
const record = value;
|
|
21
|
+
return String(record.message || record.text || record.answer || "");
|
|
22
|
+
};
|
|
23
|
+
var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
|
|
24
|
+
if (!hasAttachments) return text;
|
|
25
|
+
const marker = "Attached text file (";
|
|
26
|
+
const trimmed = text.trim();
|
|
27
|
+
if (trimmed.startsWith(marker)) return "";
|
|
28
|
+
const markerIndex = text.indexOf(`
|
|
29
|
+
|
|
30
|
+
${marker}`);
|
|
31
|
+
if (markerIndex === -1) return text;
|
|
32
|
+
return text.slice(0, markerIndex).trim();
|
|
33
|
+
};
|
|
34
|
+
var attachmentsFromTurn = (turn) => (turn.attachments || []).map((attachment, index) => {
|
|
35
|
+
const record = attachment || {};
|
|
36
|
+
return {
|
|
37
|
+
...record,
|
|
38
|
+
type: "file",
|
|
39
|
+
url: String(record.url || ""),
|
|
40
|
+
filename: String(record.filename || record.name || `attachment-${index + 1}`),
|
|
41
|
+
mediaType: String(record.mime_type || record.mediaType || record.type || "application/octet-stream")
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
var latestEventPayloadValue = (events, key) => {
|
|
45
|
+
for (let index = (events || []).length - 1; index >= 0; index -= 1) {
|
|
46
|
+
const payload = events?.[index]?.payload;
|
|
47
|
+
if (payload && payload[key] !== void 0 && payload[key] !== null) return payload[key];
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
};
|
|
51
|
+
var responseBlocksFromTurn = (turn) => {
|
|
52
|
+
if (Array.isArray(turn.response_blocks) && turn.response_blocks.length > 0) return turn.response_blocks;
|
|
53
|
+
const fromEvents = latestEventPayloadValue(turn.run_events, "response_blocks");
|
|
54
|
+
return Array.isArray(fromEvents) ? fromEvents : [];
|
|
55
|
+
};
|
|
56
|
+
var assistantTextFromEvents = (events) => {
|
|
57
|
+
const assistantText = latestEventPayloadValue(events, "assistant_output_text");
|
|
58
|
+
if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
|
|
59
|
+
return textFromFinalOutput(latestEventPayloadValue(events, "final_output"));
|
|
60
|
+
};
|
|
61
|
+
var normalizeToolStatus = (status) => {
|
|
62
|
+
if (status === "running" || status === "active" || status === "pending") return "running";
|
|
63
|
+
if (status === "failed" || status === "error") return "error";
|
|
64
|
+
return "done";
|
|
65
|
+
};
|
|
66
|
+
var toolName = (tool) => String(tool.toolName || tool.tool_name || tool.name || tool.slug || tool.action || tool.title || "").trim().toLowerCase();
|
|
67
|
+
var humanize = (value) => value.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
68
|
+
var toolActionKey = (tool) => {
|
|
69
|
+
const name = toolName(tool);
|
|
70
|
+
if (name.includes("search")) return "search";
|
|
71
|
+
if (name.includes("browse") || name.includes("navigate")) return "navigate";
|
|
72
|
+
if (name.includes("resolve")) return "resolve";
|
|
73
|
+
if (name.includes("open") || name.includes("read")) return "read";
|
|
74
|
+
if (name.includes("context")) return "context";
|
|
75
|
+
if (name.includes("link")) return "links";
|
|
76
|
+
return "work";
|
|
77
|
+
};
|
|
78
|
+
var actionLabels = {
|
|
79
|
+
search: "Searching",
|
|
80
|
+
navigate: "Navigating",
|
|
81
|
+
resolve: "Resolving",
|
|
82
|
+
read: "Reading",
|
|
83
|
+
context: "Checking context",
|
|
84
|
+
links: "Finding links",
|
|
85
|
+
work: "Working"
|
|
86
|
+
};
|
|
87
|
+
var toolTitle = (tool) => {
|
|
88
|
+
const explicit = tool.title || tool.label || tool.summary;
|
|
89
|
+
if (explicit) return String(explicit);
|
|
90
|
+
const name = toolName(tool);
|
|
91
|
+
return name ? humanize(name) : actionLabels[toolActionKey(tool)];
|
|
92
|
+
};
|
|
93
|
+
var toolDetail = (tool) => {
|
|
94
|
+
const summary = String(tool.summary || "").trim();
|
|
95
|
+
const detail = String(tool.detail || "").trim();
|
|
96
|
+
return summary || detail || void 0;
|
|
97
|
+
};
|
|
98
|
+
var summarizeToolGroup = (tools) => {
|
|
99
|
+
if (tools.length === 1) return tools[0].title;
|
|
100
|
+
const counts = /* @__PURE__ */ new Map();
|
|
101
|
+
tools.forEach((tool) => counts.set(tool.actionKey, (counts.get(tool.actionKey) || 0) + 1));
|
|
102
|
+
return Array.from(counts.entries()).map(([key, count]) => `${actionLabels[key]} ${count}`).join(", ");
|
|
103
|
+
};
|
|
104
|
+
var toolGroupStatus = (tools) => {
|
|
105
|
+
if (tools.some((tool) => tool.status === "error")) return "error";
|
|
106
|
+
if (tools.some((tool) => tool.status === "running")) return "running";
|
|
107
|
+
return "done";
|
|
108
|
+
};
|
|
109
|
+
var createToolBlock = (block, index) => {
|
|
110
|
+
const tool = block.tool || {};
|
|
111
|
+
const actionKey = toolActionKey(tool);
|
|
112
|
+
return {
|
|
113
|
+
id: String(block.id || `${block.kind || "tool"}-${index}`),
|
|
114
|
+
kind: "tool",
|
|
115
|
+
title: toolTitle(tool),
|
|
116
|
+
actionLabel: actionLabels[actionKey],
|
|
117
|
+
actionKey,
|
|
118
|
+
status: normalizeToolStatus(block.status),
|
|
119
|
+
detail: toolDetail(tool)
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
var renderBlocksFromResponseBlocks = (blocks, fallbackText) => {
|
|
123
|
+
const mapped = [];
|
|
124
|
+
let pendingTools = [];
|
|
125
|
+
const flushTools = () => {
|
|
126
|
+
if (pendingTools.length === 0) return;
|
|
127
|
+
mapped.push({
|
|
128
|
+
id: `tool-group-${pendingTools[0].id}`,
|
|
129
|
+
kind: "tool_group",
|
|
130
|
+
title: summarizeToolGroup(pendingTools),
|
|
131
|
+
status: toolGroupStatus(pendingTools),
|
|
132
|
+
tools: pendingTools
|
|
133
|
+
});
|
|
134
|
+
pendingTools = [];
|
|
135
|
+
};
|
|
136
|
+
(blocks || []).forEach((block, index) => {
|
|
137
|
+
const id = String(block.id || `${block.kind || "block"}-${index}`);
|
|
138
|
+
if (block.kind === "assistant_text" && typeof block.text === "string") {
|
|
139
|
+
flushTools();
|
|
140
|
+
mapped.push({ id, kind: "text", content: String(block.text) });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (block.kind === "tool_call") pendingTools.push(createToolBlock(block, index));
|
|
144
|
+
});
|
|
145
|
+
flushTools();
|
|
146
|
+
if (!mapped.some((block) => block.kind === "text") && fallbackText?.trim()) {
|
|
147
|
+
mapped.push({ id: "assistant-fallback-text", kind: "text", content: fallbackText });
|
|
148
|
+
}
|
|
149
|
+
return mapped;
|
|
150
|
+
};
|
|
151
|
+
var mergeReasoningSteps = (steps, options) => {
|
|
152
|
+
if (!steps?.length) return [];
|
|
153
|
+
const merged = [];
|
|
154
|
+
const indexMap = /* @__PURE__ */ new Map();
|
|
155
|
+
steps.forEach((step) => {
|
|
156
|
+
const key = String(step.label);
|
|
157
|
+
const status = options?.finalize ? "complete" : step.status || "complete";
|
|
158
|
+
if (indexMap.has(key)) {
|
|
159
|
+
const index = indexMap.get(key);
|
|
160
|
+
merged[index] = { ...merged[index], ...step, status };
|
|
161
|
+
} else {
|
|
162
|
+
indexMap.set(key, merged.push({ ...step, status }) - 1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return merged;
|
|
166
|
+
};
|
|
167
|
+
var reasoningStepsFromBlocks = (blocks) => (blocks || []).map((block) => {
|
|
168
|
+
const kind = String(block.kind || "");
|
|
169
|
+
if (kind === "tool_call") {
|
|
170
|
+
const tool = block.tool || {};
|
|
171
|
+
return {
|
|
172
|
+
label: toolTitle(tool),
|
|
173
|
+
status: block.status === "running" ? "active" : "complete",
|
|
174
|
+
description: String(tool.summary || tool.detail || tool.action || "")
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (kind === "reasoning_note") {
|
|
178
|
+
return {
|
|
179
|
+
label: String(block.label || "Reasoning"),
|
|
180
|
+
status: block.status === "running" ? "active" : "complete",
|
|
181
|
+
description: String(block.text || block.summary || "")
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}).filter(Boolean);
|
|
186
|
+
var turnToMessages = (turn, activeRunId) => {
|
|
187
|
+
const messages = [];
|
|
188
|
+
const createdAt = turn.created_at ? new Date(turn.created_at) : /* @__PURE__ */ new Date();
|
|
189
|
+
const baseId = turn.id || turn.run_id || createChatId();
|
|
190
|
+
const responseBlocks = responseBlocksFromTurn(turn);
|
|
191
|
+
const isRunning = isRunningThreadStatus(turn.status) || Boolean(turn.run_id && activeRunId === turn.run_id);
|
|
192
|
+
const attachments = attachmentsFromTurn(turn);
|
|
193
|
+
const userContent = displayTextWithoutInlineAttachments(turn.user_input_text || "", attachments.length > 0);
|
|
194
|
+
if (userContent || attachments.length > 0) {
|
|
195
|
+
messages.push({
|
|
196
|
+
id: `${baseId}-user`,
|
|
197
|
+
role: "user",
|
|
198
|
+
content: userContent,
|
|
199
|
+
createdAt,
|
|
200
|
+
attachments
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const assistantText = turn.assistant_output_text || assistantTextFromBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events) || textFromFinalOutput(turn.final_output);
|
|
204
|
+
if (assistantText || responseBlocks.length > 0 || isRunning) {
|
|
205
|
+
messages.push({
|
|
206
|
+
id: `${baseId}-assistant`,
|
|
207
|
+
role: "assistant",
|
|
208
|
+
runId: turn.run_id || null,
|
|
209
|
+
content: assistantText,
|
|
210
|
+
createdAt: turn.completed_at ? new Date(turn.completed_at) : createdAt,
|
|
211
|
+
blocks: renderBlocksFromResponseBlocks(responseBlocks, assistantText),
|
|
212
|
+
reasoningSteps: mergeReasoningSteps(reasoningStepsFromBlocks(responseBlocks), { finalize: !isRunning }),
|
|
213
|
+
isFinal: !isRunning
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return messages;
|
|
217
|
+
};
|
|
218
|
+
var threadDetailToMessages = (thread) => {
|
|
219
|
+
const activeRun = activeRunIdFromThreadDetail(thread);
|
|
220
|
+
return (thread.turns || []).flatMap((turn) => turnToMessages(turn, activeRun));
|
|
221
|
+
};
|
|
222
|
+
var threadPaging = (thread) => {
|
|
223
|
+
const nextBeforeTurnIndex = thread.paging?.next_before_turn_index ?? null;
|
|
224
|
+
return {
|
|
225
|
+
hasOlderTurns: Boolean(thread.paging?.has_more || nextBeforeTurnIndex !== null),
|
|
226
|
+
nextBeforeTurnIndex
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
var latestContextWindowFromThread = (thread) => {
|
|
230
|
+
if (thread.context_window && typeof thread.context_window === "object") return thread.context_window;
|
|
231
|
+
for (let index = (thread.turns || []).length - 1; index >= 0; index -= 1) {
|
|
232
|
+
const value = thread.turns?.[index]?.context_window;
|
|
233
|
+
if (value && typeof value === "object") return value;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
};
|
|
237
|
+
var activeRunIdFromThread = (thread) => {
|
|
238
|
+
const hasCamelActiveRun = Boolean(
|
|
239
|
+
thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
|
|
240
|
+
);
|
|
241
|
+
const hasCamelLastRunStatus = Boolean(
|
|
242
|
+
thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
|
|
243
|
+
);
|
|
244
|
+
const hasCamelLastRunId = Boolean(
|
|
245
|
+
thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
|
|
246
|
+
);
|
|
247
|
+
const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
|
|
248
|
+
const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
|
|
249
|
+
const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
|
|
250
|
+
if (activeRunId && isRunningThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
|
|
251
|
+
const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
|
|
252
|
+
return lastRunId && isRunningThreadStatus(lastRunStatus) ? String(lastRunId) : null;
|
|
253
|
+
};
|
|
254
|
+
var hasUnfinishedAssistantMessage = (messages) => Boolean(messages?.some((message) => message.role === "assistant" && message.isFinal === false));
|
|
255
|
+
var hasStaleUnfinishedAssistantCache = (thread) => hasUnfinishedAssistantMessage(thread?.messages) && !activeRunIdFromThread(thread);
|
|
256
|
+
var activeRunIdFromThreadDetail = (thread) => {
|
|
257
|
+
const summaryRunId = activeRunIdFromThread(thread);
|
|
258
|
+
if (summaryRunId) return summaryRunId;
|
|
259
|
+
const runningTurn = [...thread?.turns || []].reverse().find((turn) => turn.run_id && isRunningThreadStatus(turn.status));
|
|
260
|
+
return runningTurn?.run_id ? String(runningTurn.run_id) : null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/controller.ts
|
|
264
|
+
var mergeContextWindow = (current, incoming) => {
|
|
265
|
+
if (!incoming || typeof incoming !== "object") return current;
|
|
266
|
+
return { ...current || {}, ...incoming };
|
|
267
|
+
};
|
|
268
|
+
var threadSummaryToStored = (thread) => ({
|
|
269
|
+
...thread,
|
|
270
|
+
id: String(thread.id),
|
|
271
|
+
title: String(thread.title || "New chat"),
|
|
272
|
+
updated_at: String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString()),
|
|
273
|
+
messages: [],
|
|
274
|
+
isHydrated: false
|
|
275
|
+
});
|
|
276
|
+
function useAgents24ChatController({
|
|
277
|
+
transport,
|
|
278
|
+
storage,
|
|
279
|
+
activeThreadId: controlledActiveThreadId,
|
|
280
|
+
pageSize = DEFAULT_THREAD_PAGE_SIZE,
|
|
281
|
+
storageKey,
|
|
282
|
+
createId = createChatId,
|
|
283
|
+
onActiveThreadIdChange,
|
|
284
|
+
onSourceClick,
|
|
285
|
+
onStreamErrorMessage
|
|
286
|
+
}) {
|
|
287
|
+
const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
|
|
288
|
+
const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
|
|
289
|
+
const [messages, setMessages] = useState(() => initialCached?.messages || []);
|
|
290
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
291
|
+
const [isLoadingHistory, setIsLoadingHistory] = useState(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
|
|
292
|
+
const [isLoadingOlder, setIsLoadingOlder] = useState(false);
|
|
293
|
+
const [hasOlderTurns, setHasOlderTurns] = useState(Boolean(initialCached?.hasOlderTurns));
|
|
294
|
+
const [streamingContent, setStreamingContent] = useState("");
|
|
295
|
+
const [streamingMessageId, setStreamingMessageId] = useState(null);
|
|
296
|
+
const [contextStatus, setContextStatus] = useState(null);
|
|
297
|
+
const [currentReasoning, setCurrentReasoning] = useState([]);
|
|
298
|
+
const [liked, setLiked] = useState({});
|
|
299
|
+
const [disliked, setDisliked] = useState({});
|
|
300
|
+
const [copiedMessageId, setCopiedMessageId] = useState(null);
|
|
301
|
+
const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState(null);
|
|
302
|
+
const textareaRef = useRef(null);
|
|
303
|
+
const activeThreadIdRef = useRef(activeThreadId);
|
|
304
|
+
const messagesRef = useRef(messages);
|
|
305
|
+
const nextBeforeTurnIndexRef = useRef(initialCached?.nextBeforeTurnIndex ?? null);
|
|
306
|
+
const hasOlderTurnsRef = useRef(Boolean(initialCached?.hasOlderTurns));
|
|
307
|
+
const isLoadingOlderRef = useRef(false);
|
|
308
|
+
const loadedThreadIdRef = useRef(initialCached?.messages?.length ? activeThreadId : null);
|
|
309
|
+
const requestSeqRef = useRef(0);
|
|
310
|
+
const isLoadingHistoryRef = useRef(isLoadingHistory);
|
|
311
|
+
const activeRunIdRef = useRef(null);
|
|
312
|
+
const reattachedRunIdRef = useRef(null);
|
|
313
|
+
const abortControllerRef = useRef(null);
|
|
314
|
+
const streamingContentRef = useRef("");
|
|
315
|
+
const streamingMessageIdRef = useRef(null);
|
|
316
|
+
const reasoningRef = useRef([]);
|
|
317
|
+
const liveVoiceIdsRef = useRef({});
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
messagesRef.current = messages;
|
|
320
|
+
}, [messages]);
|
|
321
|
+
const persistThread = useCallback(
|
|
322
|
+
(threadId, nextMessages, paging, options) => {
|
|
323
|
+
const existing = storage.getThread(threadId);
|
|
324
|
+
const firstUser = nextMessages.find((message) => message.role === "user");
|
|
325
|
+
storage.upsertThread({
|
|
326
|
+
...existing || {},
|
|
327
|
+
id: threadId,
|
|
328
|
+
title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
|
|
329
|
+
updated_at: options?.updatedAt || (options?.touch ? (/* @__PURE__ */ new Date()).toISOString() : existing?.updated_at) || (/* @__PURE__ */ new Date()).toISOString(),
|
|
330
|
+
messages: nextMessages,
|
|
331
|
+
isHydrated: true,
|
|
332
|
+
hasOlderTurns: paging?.hasOlderTurns ?? hasOlderTurnsRef.current,
|
|
333
|
+
nextBeforeTurnIndex: paging?.nextBeforeTurnIndex ?? nextBeforeTurnIndexRef.current
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
[storage]
|
|
337
|
+
);
|
|
338
|
+
const markThreadRunStatus = useCallback(
|
|
339
|
+
(threadId, runId, status, lastEventSeq) => {
|
|
340
|
+
const existing = storage.getThread(threadId);
|
|
341
|
+
if (!existing || !runId) return;
|
|
342
|
+
storage.upsertThread({
|
|
343
|
+
...existing,
|
|
344
|
+
last_run_id: runId,
|
|
345
|
+
last_run_status: status,
|
|
346
|
+
active_run: {
|
|
347
|
+
run_id: runId,
|
|
348
|
+
status,
|
|
349
|
+
created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
350
|
+
},
|
|
351
|
+
lastRunId: runId,
|
|
352
|
+
lastRunStatus: status,
|
|
353
|
+
activeRun: {
|
|
354
|
+
run_id: runId,
|
|
355
|
+
status,
|
|
356
|
+
created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
357
|
+
},
|
|
358
|
+
lastEventSeq: typeof lastEventSeq === "number" ? lastEventSeq : existing.lastEventSeq ?? null,
|
|
359
|
+
isRunning: status === "queued" || status === "running"
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
[storage]
|
|
363
|
+
);
|
|
364
|
+
const refresh = useCallback(async () => {
|
|
365
|
+
const data = await transport.listThreads();
|
|
366
|
+
storage.setThreads((data.items || []).map((item) => threadSummaryToStored(item)));
|
|
367
|
+
}, [storage, transport]);
|
|
368
|
+
const applyThreadId = useCallback(
|
|
369
|
+
(threadId, baseMessages) => {
|
|
370
|
+
if (!threadId || activeThreadIdRef.current === threadId) return;
|
|
371
|
+
activeThreadIdRef.current = threadId;
|
|
372
|
+
persistThread(threadId, messagesRef.current.length > 0 ? messagesRef.current : baseMessages, void 0, { touch: true });
|
|
373
|
+
storage.setActiveThreadId?.(threadId);
|
|
374
|
+
onActiveThreadIdChange?.(threadId);
|
|
375
|
+
},
|
|
376
|
+
[onActiveThreadIdChange, persistThread, storage]
|
|
377
|
+
);
|
|
378
|
+
const setStreamingText = (value) => {
|
|
379
|
+
streamingContentRef.current = value;
|
|
380
|
+
setStreamingContent(value);
|
|
381
|
+
};
|
|
382
|
+
const setLoadingHistory = useCallback((value) => {
|
|
383
|
+
isLoadingHistoryRef.current = value;
|
|
384
|
+
setIsLoadingHistory(value);
|
|
385
|
+
}, []);
|
|
386
|
+
const setReasoningSteps = useCallback((value) => {
|
|
387
|
+
reasoningRef.current = value || [];
|
|
388
|
+
setCurrentReasoning(value || []);
|
|
389
|
+
}, []);
|
|
390
|
+
const detachActiveStream = useCallback(() => {
|
|
391
|
+
const controller = abortControllerRef.current;
|
|
392
|
+
abortControllerRef.current = null;
|
|
393
|
+
controller?.abort();
|
|
394
|
+
activeRunIdRef.current = null;
|
|
395
|
+
reattachedRunIdRef.current = null;
|
|
396
|
+
streamingMessageIdRef.current = null;
|
|
397
|
+
streamingContentRef.current = "";
|
|
398
|
+
reasoningRef.current = [];
|
|
399
|
+
setStreamingMessageId(null);
|
|
400
|
+
setIsLoading(false);
|
|
401
|
+
setStreamingContent("");
|
|
402
|
+
setCurrentReasoning([]);
|
|
403
|
+
}, []);
|
|
404
|
+
const setLiveAssistantMessage = useCallback(
|
|
405
|
+
(input) => {
|
|
406
|
+
setMessages((prev) => {
|
|
407
|
+
const index = prev.findIndex(
|
|
408
|
+
(message) => message.id === input.messageId || input.runId && message.role === "assistant" && message.runId === input.runId
|
|
409
|
+
);
|
|
410
|
+
if (index === -1) {
|
|
411
|
+
const next2 = [...prev];
|
|
412
|
+
(input.baseMessages || []).forEach((message) => {
|
|
413
|
+
const isSameAssistant = message.id === input.messageId || Boolean(input.runId && message.role === "assistant" && message.runId === input.runId);
|
|
414
|
+
if (!isSameAssistant && !next2.some((item) => item.id === message.id)) {
|
|
415
|
+
next2.push(message);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
next2.push({
|
|
419
|
+
id: input.messageId,
|
|
420
|
+
role: "assistant",
|
|
421
|
+
runId: input.runId ?? null,
|
|
422
|
+
content: input.content,
|
|
423
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
424
|
+
reasoningSteps: input.reasoning,
|
|
425
|
+
isFinal: false,
|
|
426
|
+
blocks: input.blocks
|
|
427
|
+
});
|
|
428
|
+
messagesRef.current = next2;
|
|
429
|
+
return next2;
|
|
430
|
+
}
|
|
431
|
+
const next = [...prev];
|
|
432
|
+
next[index] = {
|
|
433
|
+
...next[index],
|
|
434
|
+
runId: input.runId ?? next[index].runId ?? null,
|
|
435
|
+
content: input.content,
|
|
436
|
+
reasoningSteps: input.reasoning,
|
|
437
|
+
isFinal: false,
|
|
438
|
+
blocks: input.blocks ?? next[index].blocks
|
|
439
|
+
};
|
|
440
|
+
messagesRef.current = next;
|
|
441
|
+
return next;
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
[]
|
|
445
|
+
);
|
|
446
|
+
const finalizeAssistantMessage = useCallback(
|
|
447
|
+
(input) => {
|
|
448
|
+
const content = input.error || input.assistantText.trim();
|
|
449
|
+
if (!content) return input.baseMessages;
|
|
450
|
+
const existingIndex = input.baseMessages.findIndex(
|
|
451
|
+
(message) => input.messageId && message.id === input.messageId || input.runId && message.role === "assistant" && message.runId === input.runId
|
|
452
|
+
);
|
|
453
|
+
const existing = existingIndex >= 0 ? input.baseMessages[existingIndex] : void 0;
|
|
454
|
+
const blocks = input.blocks || existing?.blocks || renderBlocksFromResponseBlocks(void 0, content);
|
|
455
|
+
const assistant = {
|
|
456
|
+
id: existing?.id || input.messageId || createId(),
|
|
457
|
+
role: "assistant",
|
|
458
|
+
runId: input.runId ?? existing?.runId ?? null,
|
|
459
|
+
content,
|
|
460
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
461
|
+
isFinal: true,
|
|
462
|
+
blocks: blocks.map((block) => {
|
|
463
|
+
if (block.kind === "tool") return { ...block, status: input.error ? "error" : "done" };
|
|
464
|
+
if (block.kind === "tool_group") {
|
|
465
|
+
return { ...block, status: input.error ? "error" : "done", tools: block.tools.map((tool) => ({ ...tool, status: input.error ? "error" : "done" })) };
|
|
466
|
+
}
|
|
467
|
+
return block;
|
|
468
|
+
}),
|
|
469
|
+
reasoningSteps: mergeReasoningSteps(input.reasoning, { finalize: true }),
|
|
470
|
+
thinkingDurationMs: input.thinkingDurationMs
|
|
471
|
+
};
|
|
472
|
+
const completed = existingIndex >= 0 ? input.baseMessages.map((message, index) => index === existingIndex ? assistant : message) : [...input.baseMessages, assistant];
|
|
473
|
+
setMessages(completed);
|
|
474
|
+
messagesRef.current = completed;
|
|
475
|
+
if (input.threadId) {
|
|
476
|
+
persistThread(input.threadId, completed);
|
|
477
|
+
const existingThread = storage.getThread(input.threadId);
|
|
478
|
+
if (existingThread && input.runId) {
|
|
479
|
+
storage.upsertThread({
|
|
480
|
+
...existingThread,
|
|
481
|
+
messages: completed,
|
|
482
|
+
last_run_id: input.runId,
|
|
483
|
+
last_run_status: input.error ? "failed" : "completed",
|
|
484
|
+
active_run: null,
|
|
485
|
+
lastRunId: input.runId,
|
|
486
|
+
lastRunStatus: input.error ? "failed" : "completed",
|
|
487
|
+
activeRun: null,
|
|
488
|
+
isRunning: false
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return completed;
|
|
493
|
+
},
|
|
494
|
+
[createId, persistThread, storage]
|
|
495
|
+
);
|
|
496
|
+
const loadThread = useCallback(
|
|
497
|
+
async (threadId) => {
|
|
498
|
+
const seq = ++requestSeqRef.current;
|
|
499
|
+
setLoadingHistory(true);
|
|
500
|
+
setIsLoadingOlder(false);
|
|
501
|
+
isLoadingOlderRef.current = false;
|
|
502
|
+
try {
|
|
503
|
+
const detail = await transport.getThread({ threadId, limit: pageSize });
|
|
504
|
+
if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
|
|
505
|
+
const nextMessages = threadDetailToMessages(detail);
|
|
506
|
+
const paging = threadPaging(detail);
|
|
507
|
+
setContextStatus(latestContextWindowFromThread(detail));
|
|
508
|
+
setMessages(nextMessages);
|
|
509
|
+
messagesRef.current = nextMessages;
|
|
510
|
+
setHasOlderTurns(paging.hasOlderTurns);
|
|
511
|
+
hasOlderTurnsRef.current = paging.hasOlderTurns;
|
|
512
|
+
nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
|
|
513
|
+
loadedThreadIdRef.current = threadId;
|
|
514
|
+
storage.upsertThread({
|
|
515
|
+
...threadSummaryToStored(detail),
|
|
516
|
+
messages: nextMessages,
|
|
517
|
+
isHydrated: true,
|
|
518
|
+
hasOlderTurns: paging.hasOlderTurns,
|
|
519
|
+
nextBeforeTurnIndex: paging.nextBeforeTurnIndex,
|
|
520
|
+
updated_at: threadActivityDate(detail)
|
|
521
|
+
});
|
|
522
|
+
} finally {
|
|
523
|
+
if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
[pageSize, setLoadingHistory, storage, transport]
|
|
527
|
+
);
|
|
528
|
+
const loadOlderTurns = useCallback(async () => {
|
|
529
|
+
const threadId = activeThreadIdRef.current;
|
|
530
|
+
const beforeTurnIndex = nextBeforeTurnIndexRef.current;
|
|
531
|
+
if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
|
|
532
|
+
isLoadingOlderRef.current = true;
|
|
533
|
+
setIsLoadingOlder(true);
|
|
534
|
+
try {
|
|
535
|
+
const detail = await transport.getThread({ threadId, limit: pageSize, beforeTurnIndex });
|
|
536
|
+
if (activeThreadIdRef.current !== threadId) return;
|
|
537
|
+
const older = threadDetailToMessages(detail);
|
|
538
|
+
const paging = threadPaging(detail);
|
|
539
|
+
const next = [...older, ...messagesRef.current];
|
|
540
|
+
setMessages(next);
|
|
541
|
+
messagesRef.current = next;
|
|
542
|
+
setHasOlderTurns(paging.hasOlderTurns);
|
|
543
|
+
hasOlderTurnsRef.current = paging.hasOlderTurns;
|
|
544
|
+
nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
|
|
545
|
+
persistThread(threadId, next, paging);
|
|
546
|
+
} finally {
|
|
547
|
+
if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
|
|
548
|
+
isLoadingOlderRef.current = false;
|
|
549
|
+
}
|
|
550
|
+
}, [pageSize, persistThread, transport]);
|
|
551
|
+
const handleStreamEvent = useCallback(
|
|
552
|
+
(input) => {
|
|
553
|
+
const { event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
|
|
554
|
+
const payload = event.payload || {};
|
|
555
|
+
const responseBlocks = Array.isArray(payload.response_blocks) ? payload.response_blocks : null;
|
|
556
|
+
if (event.run_id) activeRunIdRef.current = event.run_id;
|
|
557
|
+
setContextStatus((current) => mergeContextWindow(current, payload.context_window));
|
|
558
|
+
if (responseBlocks) {
|
|
559
|
+
const blockText = String(payload.assistant_output_text || "") || assistantTextFromBlocks(responseBlocks) || streamingContentRef.current;
|
|
560
|
+
if (blockText) setStreamingText(blockText);
|
|
561
|
+
const reasoning = mergeReasoningSteps([...reasoningRef.current, ...reasoningStepsFromBlocks(responseBlocks)]);
|
|
562
|
+
setReasoningSteps(reasoning);
|
|
563
|
+
setLiveAssistantMessage({
|
|
564
|
+
messageId: assistantMessageId,
|
|
565
|
+
runId: event.run_id || activeRunIdRef.current,
|
|
566
|
+
content: blockText,
|
|
567
|
+
reasoning,
|
|
568
|
+
blocks: renderBlocksFromResponseBlocks(responseBlocks, blockText),
|
|
569
|
+
baseMessages
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (event.event === "run.accepted" || event.event === "run.snapshot") {
|
|
573
|
+
const nextThreadId = String(payload.thread_id || streamThreadIdRef.current || "");
|
|
574
|
+
if (nextThreadId) {
|
|
575
|
+
streamThreadIdRef.current = nextThreadId;
|
|
576
|
+
applyThreadId(nextThreadId, baseMessages);
|
|
577
|
+
markThreadRunStatus(nextThreadId, event.run_id || activeRunIdRef.current || "", String(payload.status || "running"), typeof payload.last_event_seq === "number" ? payload.last_event_seq : null);
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const isTerminal = event.event === "run.completed" || event.event === "run.cancelled" || event.event === "reshet.stream.result";
|
|
582
|
+
const isFailed = event.event === "run.failed" || event.event.endsWith(".stream.error");
|
|
583
|
+
if (!isTerminal && !isFailed) return;
|
|
584
|
+
const terminalThreadId = String(payload.thread_id || "");
|
|
585
|
+
if (terminalThreadId && !streamThreadIdRef.current) {
|
|
586
|
+
streamThreadIdRef.current = terminalThreadId;
|
|
587
|
+
applyThreadId(terminalThreadId, baseMessages);
|
|
588
|
+
}
|
|
589
|
+
const finalText = String(payload.assistant_output_text || textFromFinalOutput(payload.final_output) || streamingContentRef.current || "");
|
|
590
|
+
finalizeAssistantMessage({
|
|
591
|
+
threadId: streamThreadIdRef.current || activeThreadIdRef.current,
|
|
592
|
+
baseMessages: messagesRef.current,
|
|
593
|
+
assistantText: finalText,
|
|
594
|
+
reasoning: reasoningRef.current,
|
|
595
|
+
thinkingDurationMs: Date.now() - startedAt,
|
|
596
|
+
messageId: assistantMessageId,
|
|
597
|
+
runId: event.run_id || activeRunIdRef.current,
|
|
598
|
+
blocks: responseBlocks ? renderBlocksFromResponseBlocks(responseBlocks, finalText) : void 0,
|
|
599
|
+
error: isFailed ? String(payload.message || payload.error || event.diagnostics?.[0]?.message || onStreamErrorMessage?.(event) || "The chat run failed.") : void 0
|
|
600
|
+
});
|
|
601
|
+
},
|
|
602
|
+
[applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onStreamErrorMessage, setLiveAssistantMessage, setReasoningSteps]
|
|
603
|
+
);
|
|
604
|
+
const runStream = useCallback(
|
|
605
|
+
async (input) => {
|
|
606
|
+
const startedAt = Date.now();
|
|
607
|
+
const controller = new AbortController();
|
|
608
|
+
requestSeqRef.current += 1;
|
|
609
|
+
abortControllerRef.current = controller;
|
|
610
|
+
setIsLoading(true);
|
|
611
|
+
setStreamingText("");
|
|
612
|
+
setReasoningSteps([{ label: input.mode === "attach" ? "Reconnecting" : "Connecting", status: "active" }]);
|
|
613
|
+
const streamThreadIdRef = { current: input.mode === "attach" ? input.threadId : activeThreadIdRef.current };
|
|
614
|
+
const attachAssistantMessage = input.mode === "attach" ? [...messagesRef.current].reverse().find(
|
|
615
|
+
(message) => message.role === "assistant" && (message.runId === input.runId || message.isFinal === false)
|
|
616
|
+
) : void 0;
|
|
617
|
+
let assistantMessageId = input.mode === "attach" ? attachAssistantMessage?.id || `${input.runId}-assistant` : createId();
|
|
618
|
+
let baseMessages = messagesRef.current;
|
|
619
|
+
if (input.mode === "submit") {
|
|
620
|
+
const userMessage = {
|
|
621
|
+
id: createId(),
|
|
622
|
+
role: "user",
|
|
623
|
+
content: input.message.text,
|
|
624
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
625
|
+
attachments: input.message.files
|
|
626
|
+
};
|
|
627
|
+
baseMessages = [...messagesRef.current, userMessage];
|
|
628
|
+
}
|
|
629
|
+
if (!messagesRef.current.some((message) => message.id === assistantMessageId)) {
|
|
630
|
+
const assistantMessage = {
|
|
631
|
+
id: assistantMessageId,
|
|
632
|
+
role: "assistant",
|
|
633
|
+
content: "",
|
|
634
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
635
|
+
isFinal: false,
|
|
636
|
+
reasoningSteps: []
|
|
637
|
+
};
|
|
638
|
+
const liveMessages = [...baseMessages, assistantMessage];
|
|
639
|
+
setMessages(liveMessages);
|
|
640
|
+
messagesRef.current = liveMessages;
|
|
641
|
+
} else if (input.mode === "attach" && attachAssistantMessage) {
|
|
642
|
+
const nextMessages = messagesRef.current.map(
|
|
643
|
+
(message) => message.id === attachAssistantMessage.id ? { ...message, runId: message.runId || input.runId, isFinal: false } : message
|
|
644
|
+
);
|
|
645
|
+
setMessages(nextMessages);
|
|
646
|
+
messagesRef.current = nextMessages;
|
|
647
|
+
baseMessages = nextMessages;
|
|
648
|
+
if (streamThreadIdRef.current) persistThread(streamThreadIdRef.current, nextMessages);
|
|
649
|
+
}
|
|
650
|
+
streamingMessageIdRef.current = assistantMessageId;
|
|
651
|
+
setStreamingMessageId(assistantMessageId);
|
|
652
|
+
if (streamThreadIdRef.current) {
|
|
653
|
+
persistThread(
|
|
654
|
+
streamThreadIdRef.current,
|
|
655
|
+
messagesRef.current.length > 0 ? messagesRef.current : baseMessages,
|
|
656
|
+
void 0,
|
|
657
|
+
{ touch: input.mode === "submit" }
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
if (input.mode === "attach") {
|
|
662
|
+
activeRunIdRef.current = input.runId;
|
|
663
|
+
reattachedRunIdRef.current = input.runId;
|
|
664
|
+
await transport.attachRun(
|
|
665
|
+
{ runId: input.runId, signal: controller.signal },
|
|
666
|
+
(event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
667
|
+
);
|
|
668
|
+
} else {
|
|
669
|
+
await transport.streamMessage(
|
|
670
|
+
{ text: input.message.text, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
|
|
671
|
+
(event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
|
|
675
|
+
finalizeAssistantMessage({
|
|
676
|
+
threadId: streamThreadIdRef.current,
|
|
677
|
+
baseMessages: messagesRef.current,
|
|
678
|
+
assistantText: streamingContentRef.current,
|
|
679
|
+
reasoning: reasoningRef.current,
|
|
680
|
+
thinkingDurationMs: Date.now() - startedAt,
|
|
681
|
+
messageId: assistantMessageId,
|
|
682
|
+
runId: activeRunIdRef.current
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
await refresh().catch(() => void 0);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error.name !== "AbortError") {
|
|
688
|
+
finalizeAssistantMessage({
|
|
689
|
+
threadId: streamThreadIdRef.current,
|
|
690
|
+
baseMessages: messagesRef.current,
|
|
691
|
+
assistantText: streamingContentRef.current,
|
|
692
|
+
reasoning: reasoningRef.current,
|
|
693
|
+
thinkingDurationMs: Date.now() - startedAt,
|
|
694
|
+
messageId: assistantMessageId,
|
|
695
|
+
runId: activeRunIdRef.current,
|
|
696
|
+
error: onStreamErrorMessage?.(error) || error.message || "Failed to reach the chat agent."
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
} finally {
|
|
700
|
+
const isCurrentStream = abortControllerRef.current === controller || streamingMessageIdRef.current === assistantMessageId;
|
|
701
|
+
if (abortControllerRef.current === controller) abortControllerRef.current = null;
|
|
702
|
+
if (isCurrentStream) {
|
|
703
|
+
activeRunIdRef.current = null;
|
|
704
|
+
streamingMessageIdRef.current = null;
|
|
705
|
+
reattachedRunIdRef.current = null;
|
|
706
|
+
setStreamingMessageId(null);
|
|
707
|
+
setIsLoading(false);
|
|
708
|
+
setStreamingText("");
|
|
709
|
+
setReasoningSteps([]);
|
|
710
|
+
setLastThinkingDurationMs(Date.now() - startedAt);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
[createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setReasoningSteps, transport]
|
|
715
|
+
);
|
|
716
|
+
const handleSubmit = useCallback(
|
|
717
|
+
async (message) => {
|
|
718
|
+
if (!message.text.trim() && !(message.files || []).length) return;
|
|
719
|
+
await runStream({ mode: "submit", message });
|
|
720
|
+
},
|
|
721
|
+
[runStream]
|
|
722
|
+
);
|
|
723
|
+
const handleStop = useCallback(() => {
|
|
724
|
+
const runId = activeRunIdRef.current;
|
|
725
|
+
const partial = streamingContentRef.current;
|
|
726
|
+
const liveMessageId = streamingMessageIdRef.current;
|
|
727
|
+
abortControllerRef.current?.abort();
|
|
728
|
+
abortControllerRef.current = null;
|
|
729
|
+
activeRunIdRef.current = null;
|
|
730
|
+
streamingMessageIdRef.current = null;
|
|
731
|
+
setStreamingMessageId(null);
|
|
732
|
+
setIsLoading(false);
|
|
733
|
+
setStreamingText("");
|
|
734
|
+
setReasoningSteps([]);
|
|
735
|
+
if (runId) transport.cancelRun({ runId, assistantOutputText: partial }).catch(() => void 0);
|
|
736
|
+
if (partial.trim()) {
|
|
737
|
+
finalizeAssistantMessage({
|
|
738
|
+
threadId: activeThreadIdRef.current,
|
|
739
|
+
baseMessages: messagesRef.current,
|
|
740
|
+
assistantText: partial,
|
|
741
|
+
reasoning: reasoningRef.current,
|
|
742
|
+
thinkingDurationMs: lastThinkingDurationMs || 0,
|
|
743
|
+
messageId: liveMessageId
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}, [finalizeAssistantMessage, lastThinkingDurationMs, setReasoningSteps, transport]);
|
|
747
|
+
useEffect(() => {
|
|
748
|
+
refresh().catch(() => storage.setThreads([]));
|
|
749
|
+
}, [refresh, storage]);
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const previous = activeThreadIdRef.current;
|
|
752
|
+
activeThreadIdRef.current = activeThreadId;
|
|
753
|
+
if (activeThreadId && previous === activeThreadId && (activeRunIdRef.current || streamingMessageIdRef.current)) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (previous && previous !== activeThreadId && abortControllerRef.current) {
|
|
757
|
+
detachActiveStream();
|
|
758
|
+
}
|
|
759
|
+
if (!activeThreadId) {
|
|
760
|
+
requestSeqRef.current += 1;
|
|
761
|
+
loadedThreadIdRef.current = null;
|
|
762
|
+
setMessages([]);
|
|
763
|
+
messagesRef.current = [];
|
|
764
|
+
setHasOlderTurns(false);
|
|
765
|
+
setIsLoadingOlder(false);
|
|
766
|
+
setContextStatus(null);
|
|
767
|
+
nextBeforeTurnIndexRef.current = null;
|
|
768
|
+
setLoadingHistory(false);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const cached = storage.getThread(activeThreadId);
|
|
772
|
+
const cachedMessages = cached?.isHydrated && cached.messages?.length ? cached.messages : null;
|
|
773
|
+
const hasCachedMessages = Boolean(cachedMessages);
|
|
774
|
+
const shouldRefreshStaleCache = hasCachedMessages && hasStaleUnfinishedAssistantCache(cached);
|
|
775
|
+
if (cached && cachedMessages) {
|
|
776
|
+
setMessages(cachedMessages);
|
|
777
|
+
messagesRef.current = cachedMessages;
|
|
778
|
+
setHasOlderTurns(Boolean(cached.hasOlderTurns));
|
|
779
|
+
hasOlderTurnsRef.current = Boolean(cached.hasOlderTurns);
|
|
780
|
+
nextBeforeTurnIndexRef.current = cached.nextBeforeTurnIndex ?? null;
|
|
781
|
+
loadedThreadIdRef.current = activeThreadId;
|
|
782
|
+
setLoadingHistory(false);
|
|
783
|
+
if (!shouldRefreshStaleCache) return;
|
|
784
|
+
}
|
|
785
|
+
if (!hasCachedMessages) {
|
|
786
|
+
setMessages([]);
|
|
787
|
+
messagesRef.current = [];
|
|
788
|
+
}
|
|
789
|
+
void loadThread(activeThreadId).catch(() => setLoadingHistory(false));
|
|
790
|
+
}, [activeThreadId, detachActiveStream, loadThread, setLoadingHistory, storage]);
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
const threadId = activeThreadId;
|
|
793
|
+
if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
|
|
794
|
+
const runId = activeRunIdFromThread(storage.getThread(threadId));
|
|
795
|
+
if (!runId || reattachedRunIdRef.current === runId) return;
|
|
796
|
+
void runStream({ mode: "attach", threadId, runId });
|
|
797
|
+
}, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
|
|
798
|
+
useEffect(() => {
|
|
799
|
+
const threadId = activeThreadId;
|
|
800
|
+
if (!threadId || isLoadingHistoryRef.current || activeRunIdRef.current || streamingMessageIdRef.current) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const cached = storage.getThread(threadId);
|
|
804
|
+
if (!hasStaleUnfinishedAssistantCache(cached)) return;
|
|
805
|
+
void loadThread(threadId).catch(() => setLoadingHistory(false));
|
|
806
|
+
}, [activeThreadId, loadThread, setLoadingHistory, storage, storageKey]);
|
|
807
|
+
const handleCopy = useCallback((content, messageId) => {
|
|
808
|
+
navigator.clipboard?.writeText(content);
|
|
809
|
+
setCopiedMessageId(messageId);
|
|
810
|
+
setTimeout(() => setCopiedMessageId(null), 200);
|
|
811
|
+
}, []);
|
|
812
|
+
const handleLike = useCallback(async (msg) => {
|
|
813
|
+
const nextLiked = !liked[msg.id];
|
|
814
|
+
setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
|
|
815
|
+
if (nextLiked) setDisliked((prev) => ({ ...prev, [msg.id]: false }));
|
|
816
|
+
}, [liked]);
|
|
817
|
+
const handleDislike = useCallback(async (msg) => {
|
|
818
|
+
const nextDisliked = !disliked[msg.id];
|
|
819
|
+
setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
|
|
820
|
+
if (nextDisliked) setLiked((prev) => ({ ...prev, [msg.id]: false }));
|
|
821
|
+
}, [disliked]);
|
|
822
|
+
const handleRetry = useCallback(async (msg) => {
|
|
823
|
+
const index = messagesRef.current.findIndex((message) => message.id === msg.id);
|
|
824
|
+
if (index <= 0) return;
|
|
825
|
+
const userMessage = messagesRef.current[index - 1];
|
|
826
|
+
if (userMessage.role !== "user") return;
|
|
827
|
+
const trimmed = messagesRef.current.slice(0, index);
|
|
828
|
+
setMessages(trimmed);
|
|
829
|
+
messagesRef.current = trimmed;
|
|
830
|
+
if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, trimmed);
|
|
831
|
+
await handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
|
|
832
|
+
}, [handleSubmit, persistThread]);
|
|
833
|
+
const upsertLiveVoiceMessage = useCallback((input) => {
|
|
834
|
+
const content = input.content?.trim() ?? "";
|
|
835
|
+
if (!content && !input.citations?.length && !input.reasoningSteps?.length) return;
|
|
836
|
+
setMessages((prev) => {
|
|
837
|
+
const currentId = liveVoiceIdsRef.current[input.role];
|
|
838
|
+
const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
|
|
839
|
+
const nextMessage = {
|
|
840
|
+
id: currentId || createId(),
|
|
841
|
+
role: input.role,
|
|
842
|
+
content,
|
|
843
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
844
|
+
isFinal: Boolean(input.isFinal),
|
|
845
|
+
isVoice: input.role === "user",
|
|
846
|
+
citations: input.citations,
|
|
847
|
+
reasoningSteps: input.reasoningSteps
|
|
848
|
+
};
|
|
849
|
+
const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
|
|
850
|
+
liveVoiceIdsRef.current[input.role] = input.isFinal ? void 0 : nextMessage.id;
|
|
851
|
+
messagesRef.current = next;
|
|
852
|
+
if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, next);
|
|
853
|
+
return next;
|
|
854
|
+
});
|
|
855
|
+
}, [createId, persistThread]);
|
|
856
|
+
return useMemo(() => ({
|
|
857
|
+
messages,
|
|
858
|
+
streamingContent,
|
|
859
|
+
streamingMessageId,
|
|
860
|
+
contextStatus,
|
|
861
|
+
currentReasoning,
|
|
862
|
+
isLoading,
|
|
863
|
+
isLoadingHistory,
|
|
864
|
+
isLoadingOlder,
|
|
865
|
+
hasOlderTurns,
|
|
866
|
+
liked,
|
|
867
|
+
disliked,
|
|
868
|
+
copiedMessageId,
|
|
869
|
+
lastThinkingDurationMs,
|
|
870
|
+
activeRunId: activeRunIdRef.current,
|
|
871
|
+
handleSubmit,
|
|
872
|
+
handleStop,
|
|
873
|
+
handleCopy,
|
|
874
|
+
handleLike,
|
|
875
|
+
handleDislike,
|
|
876
|
+
handleRetry,
|
|
877
|
+
handleSourceClick: (citations) => onSourceClick?.(citations),
|
|
878
|
+
upsertLiveVoiceMessage,
|
|
879
|
+
loadOlderTurns,
|
|
880
|
+
refresh,
|
|
881
|
+
textareaRef
|
|
882
|
+
}), [
|
|
883
|
+
copiedMessageId,
|
|
884
|
+
contextStatus,
|
|
885
|
+
currentReasoning,
|
|
886
|
+
disliked,
|
|
887
|
+
handleCopy,
|
|
888
|
+
handleDislike,
|
|
889
|
+
handleLike,
|
|
890
|
+
handleRetry,
|
|
891
|
+
handleStop,
|
|
892
|
+
handleSubmit,
|
|
893
|
+
hasOlderTurns,
|
|
894
|
+
isLoading,
|
|
895
|
+
isLoadingHistory,
|
|
896
|
+
isLoadingOlder,
|
|
897
|
+
lastThinkingDurationMs,
|
|
898
|
+
liked,
|
|
899
|
+
loadOlderTurns,
|
|
900
|
+
messages,
|
|
901
|
+
onSourceClick,
|
|
902
|
+
refresh,
|
|
903
|
+
streamingContent,
|
|
904
|
+
streamingMessageId,
|
|
905
|
+
upsertLiveVoiceMessage
|
|
906
|
+
]);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/sse.ts
|
|
910
|
+
var parseSseBlock = (block) => {
|
|
911
|
+
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.replace(/^data:\s?/, "")).join("\n").trim();
|
|
912
|
+
if (!data) return null;
|
|
913
|
+
return JSON.parse(data);
|
|
914
|
+
};
|
|
915
|
+
var consumeSseResponse = async (response, onEvent) => {
|
|
916
|
+
if (!response.ok) {
|
|
917
|
+
let message = response.statusText || "Failed to open chat stream.";
|
|
918
|
+
try {
|
|
919
|
+
const errorData = await response.json();
|
|
920
|
+
message = String(errorData.detail || errorData.message || message);
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
throw new Error(message);
|
|
924
|
+
}
|
|
925
|
+
const reader = response.body?.getReader();
|
|
926
|
+
if (!reader) throw new Error("The chat stream did not return a readable body.");
|
|
927
|
+
const decoder = new TextDecoder();
|
|
928
|
+
let buffer = "";
|
|
929
|
+
let threadId = null;
|
|
930
|
+
let runId = null;
|
|
931
|
+
while (true) {
|
|
932
|
+
const { value, done } = await reader.read();
|
|
933
|
+
if (done) break;
|
|
934
|
+
buffer += decoder.decode(value, { stream: true });
|
|
935
|
+
let boundary = buffer.indexOf("\n\n");
|
|
936
|
+
while (boundary !== -1) {
|
|
937
|
+
const block = buffer.slice(0, boundary);
|
|
938
|
+
buffer = buffer.slice(boundary + 2);
|
|
939
|
+
boundary = buffer.indexOf("\n\n");
|
|
940
|
+
const event = parseSseBlock(block);
|
|
941
|
+
if (!event) continue;
|
|
942
|
+
if (event.run_id) runId = event.run_id;
|
|
943
|
+
const payloadThreadId = event.payload?.thread_id;
|
|
944
|
+
if (payloadThreadId) threadId = String(payloadThreadId);
|
|
945
|
+
await onEvent(event);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return { threadId, runId };
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// src/transport.ts
|
|
952
|
+
var jsonHeaders = (headers) => ({
|
|
953
|
+
...headers || {},
|
|
954
|
+
Accept: "application/json",
|
|
955
|
+
"Content-Type": "application/json"
|
|
956
|
+
});
|
|
957
|
+
var streamHeaders = (headers) => ({
|
|
958
|
+
...headers || {},
|
|
959
|
+
Accept: "text/event-stream",
|
|
960
|
+
"Content-Type": "application/json"
|
|
961
|
+
});
|
|
962
|
+
var createFetchChatTransport = ({
|
|
963
|
+
routes,
|
|
964
|
+
fetchImpl = fetch,
|
|
965
|
+
headers,
|
|
966
|
+
streamBody
|
|
967
|
+
}) => {
|
|
968
|
+
const loadHeaders = async () => headers?.() ?? {};
|
|
969
|
+
return {
|
|
970
|
+
async listThreads(input) {
|
|
971
|
+
const response = await fetchImpl(routes.listThreads(), {
|
|
972
|
+
signal: input?.signal,
|
|
973
|
+
headers: jsonHeaders(await loadHeaders())
|
|
974
|
+
});
|
|
975
|
+
if (!response.ok) throw new Error(response.statusText || "Failed to list chat threads.");
|
|
976
|
+
return response.json();
|
|
977
|
+
},
|
|
978
|
+
async getThread(input) {
|
|
979
|
+
const response = await fetchImpl(routes.getThread(input), {
|
|
980
|
+
signal: input.signal,
|
|
981
|
+
headers: jsonHeaders(await loadHeaders())
|
|
982
|
+
});
|
|
983
|
+
if (!response.ok) throw new Error(response.statusText || "Failed to load chat thread.");
|
|
984
|
+
return response.json();
|
|
985
|
+
},
|
|
986
|
+
async streamMessage(input, onEvent) {
|
|
987
|
+
const response = await fetchImpl(routes.streamMessage(), {
|
|
988
|
+
method: "POST",
|
|
989
|
+
signal: input.signal,
|
|
990
|
+
headers: streamHeaders(await loadHeaders()),
|
|
991
|
+
body: JSON.stringify(
|
|
992
|
+
streamBody?.(input) ?? {
|
|
993
|
+
input: input.text,
|
|
994
|
+
thread_id: input.threadId || void 0,
|
|
995
|
+
files: input.files || []
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
});
|
|
999
|
+
return consumeSseResponse(response, onEvent);
|
|
1000
|
+
},
|
|
1001
|
+
async attachRun(input, onEvent) {
|
|
1002
|
+
const response = await fetchImpl(routes.attachRun(input), {
|
|
1003
|
+
method: "POST",
|
|
1004
|
+
signal: input.signal,
|
|
1005
|
+
headers: streamHeaders(await loadHeaders()),
|
|
1006
|
+
body: JSON.stringify({})
|
|
1007
|
+
});
|
|
1008
|
+
return consumeSseResponse(response, onEvent);
|
|
1009
|
+
},
|
|
1010
|
+
async cancelRun(input) {
|
|
1011
|
+
const response = await fetchImpl(routes.cancelRun(input), {
|
|
1012
|
+
method: "POST",
|
|
1013
|
+
headers: jsonHeaders(await loadHeaders()),
|
|
1014
|
+
body: JSON.stringify({ assistant_output_text: input.assistantOutputText || void 0 })
|
|
1015
|
+
});
|
|
1016
|
+
if (!response.ok) throw new Error(response.statusText || "Failed to cancel chat run.");
|
|
1017
|
+
return response.json();
|
|
1018
|
+
},
|
|
1019
|
+
async deleteThread(input) {
|
|
1020
|
+
if (!routes.deleteThread) return { deleted: false };
|
|
1021
|
+
const response = await fetchImpl(routes.deleteThread(input), {
|
|
1022
|
+
method: "DELETE",
|
|
1023
|
+
headers: jsonHeaders(await loadHeaders())
|
|
1024
|
+
});
|
|
1025
|
+
if (!response.ok) throw new Error(response.statusText || "Failed to delete chat thread.");
|
|
1026
|
+
return response.json();
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// src/viewport.tsx
|
|
1032
|
+
import {
|
|
1033
|
+
useCallback as useCallback2,
|
|
1034
|
+
useEffect as useEffect2,
|
|
1035
|
+
useLayoutEffect,
|
|
1036
|
+
useMemo as useMemo2,
|
|
1037
|
+
useRef as useRef2,
|
|
1038
|
+
useState as useState2
|
|
1039
|
+
} from "react";
|
|
1040
|
+
import { jsxs } from "react/jsx-runtime";
|
|
1041
|
+
var LATEST_EDGE_THRESHOLD_PX = 2;
|
|
1042
|
+
var MIN_OLDER_PREFETCH_PX = 320;
|
|
1043
|
+
var MAX_OLDER_PREFETCH_PX = 900;
|
|
1044
|
+
var isScrollable = (element) => element.scrollHeight - element.clientHeight > LATEST_EDGE_THRESHOLD_PX;
|
|
1045
|
+
var isAtTimelineLatestEdge = (element, isTopOrigin) => {
|
|
1046
|
+
if (!isScrollable(element)) return true;
|
|
1047
|
+
return isTopOrigin ? element.scrollHeight - element.clientHeight - element.scrollTop <= LATEST_EDGE_THRESHOLD_PX : Math.abs(element.scrollTop) <= LATEST_EDGE_THRESHOLD_PX;
|
|
1048
|
+
};
|
|
1049
|
+
var shouldPrefetchOlder = (element) => Math.abs(element.scrollTop) + element.clientHeight >= element.scrollHeight - Math.min(MAX_OLDER_PREFETCH_PX, Math.max(MIN_OLDER_PREFETCH_PX, element.clientHeight * 0.75));
|
|
1050
|
+
var getLatestScrollTop = (element, isTopOrigin) => isTopOrigin ? Math.max(0, element.scrollHeight - element.clientHeight) : 0;
|
|
1051
|
+
function useLatestThreadViewport({
|
|
1052
|
+
itemCount,
|
|
1053
|
+
hasOlder,
|
|
1054
|
+
isLoadingOlder,
|
|
1055
|
+
onLoadOlder,
|
|
1056
|
+
activeStreamKey,
|
|
1057
|
+
shouldAutoFollow = true,
|
|
1058
|
+
topOriginMaxItems = 4
|
|
1059
|
+
}) {
|
|
1060
|
+
const scrollContainerRef = useRef2(null);
|
|
1061
|
+
const olderPagePreserveRef = useRef2(null);
|
|
1062
|
+
const olderPageRequestInFlightRef = useRef2(false);
|
|
1063
|
+
const intrinsicResizePreserveRef = useRef2(null);
|
|
1064
|
+
const autoFollowLatestRef = useRef2(true);
|
|
1065
|
+
const programmaticScrollRef = useRef2(false);
|
|
1066
|
+
const programmaticScrollTimeoutRef = useRef2(null);
|
|
1067
|
+
const activeStreamKeyRef = useRef2(null);
|
|
1068
|
+
const [isAtLatest, setIsAtLatest] = useState2(true);
|
|
1069
|
+
const [topOriginCandidateOverflows, setTopOriginCandidateOverflows] = useState2(false);
|
|
1070
|
+
const topOriginCandidate = !hasOlder && itemCount > 0 && itemCount <= topOriginMaxItems;
|
|
1071
|
+
const isTopOrigin = topOriginCandidate && !topOriginCandidateOverflows;
|
|
1072
|
+
const markProgrammaticScroll = useCallback2(() => {
|
|
1073
|
+
programmaticScrollRef.current = true;
|
|
1074
|
+
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
1075
|
+
programmaticScrollTimeoutRef.current = setTimeout(() => {
|
|
1076
|
+
programmaticScrollRef.current = false;
|
|
1077
|
+
programmaticScrollTimeoutRef.current = null;
|
|
1078
|
+
}, 80);
|
|
1079
|
+
}, []);
|
|
1080
|
+
const scrollToLatest = useCallback2(() => {
|
|
1081
|
+
const element = scrollContainerRef.current;
|
|
1082
|
+
if (!element) return;
|
|
1083
|
+
markProgrammaticScroll();
|
|
1084
|
+
element.scrollTop = getLatestScrollTop(element, isTopOrigin);
|
|
1085
|
+
setIsAtLatest(true);
|
|
1086
|
+
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1087
|
+
useEffect2(() => {
|
|
1088
|
+
return () => {
|
|
1089
|
+
if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
|
|
1090
|
+
const preserve = intrinsicResizePreserveRef.current;
|
|
1091
|
+
if (preserve?.frame !== null && preserve?.frame !== void 0) cancelAnimationFrame(preserve.frame);
|
|
1092
|
+
};
|
|
1093
|
+
}, []);
|
|
1094
|
+
useLayoutEffect(() => {
|
|
1095
|
+
const element = scrollContainerRef.current;
|
|
1096
|
+
const preserve = olderPagePreserveRef.current;
|
|
1097
|
+
if (!element) return;
|
|
1098
|
+
if (topOriginCandidate) {
|
|
1099
|
+
setTopOriginCandidateOverflows(isScrollable(element));
|
|
1100
|
+
} else if (topOriginCandidateOverflows) {
|
|
1101
|
+
setTopOriginCandidateOverflows(false);
|
|
1102
|
+
}
|
|
1103
|
+
if (!preserve) {
|
|
1104
|
+
setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const addedHeight = element.scrollHeight - preserve.previousHeight;
|
|
1108
|
+
markProgrammaticScroll();
|
|
1109
|
+
element.scrollTop = preserve.previousTop - addedHeight;
|
|
1110
|
+
olderPagePreserveRef.current = null;
|
|
1111
|
+
olderPageRequestInFlightRef.current = false;
|
|
1112
|
+
setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
|
|
1113
|
+
}, [isTopOrigin, itemCount, markProgrammaticScroll, topOriginCandidate, topOriginCandidateOverflows]);
|
|
1114
|
+
const handleScroll = useCallback2(
|
|
1115
|
+
(event) => {
|
|
1116
|
+
const element = event.currentTarget;
|
|
1117
|
+
const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
|
|
1118
|
+
setIsAtLatest(atLatest);
|
|
1119
|
+
if (!programmaticScrollRef.current) autoFollowLatestRef.current = atLatest;
|
|
1120
|
+
if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
olderPageRequestInFlightRef.current = true;
|
|
1124
|
+
olderPagePreserveRef.current = {
|
|
1125
|
+
previousHeight: element.scrollHeight,
|
|
1126
|
+
previousTop: element.scrollTop
|
|
1127
|
+
};
|
|
1128
|
+
void Promise.resolve(onLoadOlder()).finally(() => {
|
|
1129
|
+
requestAnimationFrame(() => {
|
|
1130
|
+
if (!olderPagePreserveRef.current) return;
|
|
1131
|
+
olderPagePreserveRef.current = null;
|
|
1132
|
+
olderPageRequestInFlightRef.current = false;
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
},
|
|
1136
|
+
[hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
|
|
1137
|
+
);
|
|
1138
|
+
const preserveIntrinsicResize = useCallback2(() => {
|
|
1139
|
+
const element = scrollContainerRef.current;
|
|
1140
|
+
if (!element || isTopOrigin) return;
|
|
1141
|
+
const active = intrinsicResizePreserveRef.current;
|
|
1142
|
+
if (active?.frame !== null && active?.frame !== void 0) cancelAnimationFrame(active.frame);
|
|
1143
|
+
intrinsicResizePreserveRef.current = {
|
|
1144
|
+
lastHeight: element.scrollHeight,
|
|
1145
|
+
endAt: Date.now() + 260,
|
|
1146
|
+
frame: null
|
|
1147
|
+
};
|
|
1148
|
+
const preserveFrame = () => {
|
|
1149
|
+
const nextElement = scrollContainerRef.current;
|
|
1150
|
+
const preserve = intrinsicResizePreserveRef.current;
|
|
1151
|
+
if (!nextElement || !preserve) return;
|
|
1152
|
+
const heightDelta = nextElement.scrollHeight - preserve.lastHeight;
|
|
1153
|
+
if (Math.abs(heightDelta) >= 1) {
|
|
1154
|
+
markProgrammaticScroll();
|
|
1155
|
+
nextElement.scrollTop -= heightDelta;
|
|
1156
|
+
preserve.lastHeight = nextElement.scrollHeight;
|
|
1157
|
+
setIsAtLatest(isAtTimelineLatestEdge(nextElement, isTopOrigin));
|
|
1158
|
+
}
|
|
1159
|
+
if (Date.now() < preserve.endAt) {
|
|
1160
|
+
preserve.frame = requestAnimationFrame(preserveFrame);
|
|
1161
|
+
} else {
|
|
1162
|
+
intrinsicResizePreserveRef.current = null;
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
|
|
1166
|
+
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1167
|
+
useEffect2(() => {
|
|
1168
|
+
const key = activeStreamKey || null;
|
|
1169
|
+
if (!key || activeStreamKeyRef.current === key) {
|
|
1170
|
+
activeStreamKeyRef.current = key;
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
autoFollowLatestRef.current = true;
|
|
1174
|
+
const frame = requestAnimationFrame(scrollToLatest);
|
|
1175
|
+
activeStreamKeyRef.current = key;
|
|
1176
|
+
return () => cancelAnimationFrame(frame);
|
|
1177
|
+
}, [activeStreamKey, scrollToLatest]);
|
|
1178
|
+
useLayoutEffect(() => {
|
|
1179
|
+
if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
|
|
1180
|
+
if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
|
|
1181
|
+
const frame = requestAnimationFrame(() => {
|
|
1182
|
+
if (autoFollowLatestRef.current && !isLoadingOlder && !olderPageRequestInFlightRef.current) scrollToLatest();
|
|
1183
|
+
});
|
|
1184
|
+
return () => cancelAnimationFrame(frame);
|
|
1185
|
+
}, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
|
|
1186
|
+
return {
|
|
1187
|
+
scrollContainerRef,
|
|
1188
|
+
isAtLatest,
|
|
1189
|
+
isTopOrigin,
|
|
1190
|
+
shouldShowLatestButton: !isAtLatest,
|
|
1191
|
+
handleScroll,
|
|
1192
|
+
scrollToLatest,
|
|
1193
|
+
preserveIntrinsicResize
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function LatestThreadViewport({
|
|
1197
|
+
items,
|
|
1198
|
+
hasOlder,
|
|
1199
|
+
isLoadingOlder,
|
|
1200
|
+
onLoadOlder,
|
|
1201
|
+
activeStreamKey,
|
|
1202
|
+
className,
|
|
1203
|
+
latestSpacer,
|
|
1204
|
+
trailingSpacer,
|
|
1205
|
+
renderItem
|
|
1206
|
+
}) {
|
|
1207
|
+
const viewport = useLatestThreadViewport({
|
|
1208
|
+
itemCount: items.length,
|
|
1209
|
+
hasOlder,
|
|
1210
|
+
isLoadingOlder,
|
|
1211
|
+
onLoadOlder,
|
|
1212
|
+
activeStreamKey
|
|
1213
|
+
});
|
|
1214
|
+
const timelineItems = useMemo2(() => viewport.isTopOrigin ? items : [...items].reverse(), [items, viewport.isTopOrigin]);
|
|
1215
|
+
return /* @__PURE__ */ jsxs(
|
|
1216
|
+
"div",
|
|
1217
|
+
{
|
|
1218
|
+
ref: viewport.scrollContainerRef,
|
|
1219
|
+
className,
|
|
1220
|
+
onScroll: viewport.handleScroll,
|
|
1221
|
+
role: "log",
|
|
1222
|
+
style: { overflowAnchor: "none" },
|
|
1223
|
+
children: [
|
|
1224
|
+
!viewport.isTopOrigin && (latestSpacer ?? null),
|
|
1225
|
+
timelineItems.map((item) => renderItem(item, viewport)),
|
|
1226
|
+
viewport.isTopOrigin && (trailingSpacer ?? latestSpacer ?? null)
|
|
1227
|
+
]
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
export {
|
|
1232
|
+
DEFAULT_THREAD_PAGE_SIZE,
|
|
1233
|
+
LatestThreadViewport,
|
|
1234
|
+
activeRunIdFromThread,
|
|
1235
|
+
activeRunIdFromThreadDetail,
|
|
1236
|
+
assistantTextFromBlocks,
|
|
1237
|
+
consumeSseResponse,
|
|
1238
|
+
createChatId,
|
|
1239
|
+
createFetchChatTransport,
|
|
1240
|
+
getLatestScrollTop,
|
|
1241
|
+
hasStaleUnfinishedAssistantCache,
|
|
1242
|
+
hasUnfinishedAssistantMessage,
|
|
1243
|
+
isAtTimelineLatestEdge,
|
|
1244
|
+
isRunningThreadStatus,
|
|
1245
|
+
isScrollable,
|
|
1246
|
+
latestContextWindowFromThread,
|
|
1247
|
+
mergeReasoningSteps,
|
|
1248
|
+
parseSseBlock,
|
|
1249
|
+
reasoningStepsFromBlocks,
|
|
1250
|
+
renderBlocksFromResponseBlocks,
|
|
1251
|
+
shouldPrefetchOlder,
|
|
1252
|
+
textFromFinalOutput,
|
|
1253
|
+
threadActivityDate,
|
|
1254
|
+
threadDetailToMessages,
|
|
1255
|
+
threadPaging,
|
|
1256
|
+
titleFromMessage,
|
|
1257
|
+
useAgents24ChatController,
|
|
1258
|
+
useLatestThreadViewport
|
|
1259
|
+
};
|
|
1260
|
+
//# sourceMappingURL=index.js.map
|