@lota-sdk/ui 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"registry": "https://registry.npmjs.org/"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@lota-sdk/shared": "0.3.
|
|
28
|
+
"@lota-sdk/shared": "0.3.2",
|
|
29
29
|
"ai": "^6.0.145"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { UseChatHelpers } from '@ai-sdk/react'
|
|
2
|
+
import { AbstractChat } from 'ai'
|
|
3
|
+
import type { ChatInit, ChatState, ChatStatus, UIMessage } from 'ai'
|
|
4
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'
|
|
5
|
+
|
|
6
|
+
function throttleCallback(callback: () => void, waitMs?: number): () => void {
|
|
7
|
+
if (waitMs == null) {
|
|
8
|
+
return callback
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
12
|
+
let hasPendingCall = false
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
if (timeoutId !== null) {
|
|
16
|
+
hasPendingCall = true
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
callback()
|
|
21
|
+
timeoutId = setTimeout(() => {
|
|
22
|
+
timeoutId = null
|
|
23
|
+
|
|
24
|
+
if (!hasPendingCall) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
hasPendingCall = false
|
|
29
|
+
callback()
|
|
30
|
+
}, waitMs)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class SnapshottingChatState<UI_MESSAGE extends UIMessage> implements ChatState<UI_MESSAGE> {
|
|
35
|
+
#messages: UI_MESSAGE[]
|
|
36
|
+
#status: ChatStatus = 'ready'
|
|
37
|
+
#error: Error | undefined = undefined
|
|
38
|
+
|
|
39
|
+
#messagesCallbacks = new Set<() => void>()
|
|
40
|
+
#statusCallbacks = new Set<() => void>()
|
|
41
|
+
#errorCallbacks = new Set<() => void>()
|
|
42
|
+
|
|
43
|
+
constructor(initialMessages: UI_MESSAGE[] = []) {
|
|
44
|
+
this.#messages = initialMessages
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get status(): ChatStatus {
|
|
48
|
+
return this.#status
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
set status(newStatus: ChatStatus) {
|
|
52
|
+
this.#status = newStatus
|
|
53
|
+
this.#callStatusCallbacks()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get error(): Error | undefined {
|
|
57
|
+
return this.#error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
set error(newError: Error | undefined) {
|
|
61
|
+
this.#error = newError
|
|
62
|
+
this.#callErrorCallbacks()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get messages(): UI_MESSAGE[] {
|
|
66
|
+
return this.#messages
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set messages(newMessages: UI_MESSAGE[]) {
|
|
70
|
+
this.#messages = [...newMessages]
|
|
71
|
+
this.#callMessagesCallbacks()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pushMessage = (message: UI_MESSAGE) => {
|
|
75
|
+
const lastMessage = this.#messages.at(-1)
|
|
76
|
+
|
|
77
|
+
if (lastMessage?.role === 'assistant' && message.role === 'assistant' && lastMessage.id !== message.id) {
|
|
78
|
+
// The AI SDK reuses one mutable assistant message object across consecutive `start` events
|
|
79
|
+
// in a single stream. When a second assistant starts, clear inherited parts so the new
|
|
80
|
+
// message begins empty instead of briefly rendering the previous assistant's content.
|
|
81
|
+
message.parts = [] as UI_MESSAGE['parts']
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Snapshot on insert so later stream mutations cannot rewrite already-rendered messages.
|
|
85
|
+
this.#messages = this.#messages.concat(this.snapshot(message))
|
|
86
|
+
this.#callMessagesCallbacks()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
popMessage = () => {
|
|
90
|
+
this.#messages = this.#messages.slice(0, -1)
|
|
91
|
+
this.#callMessagesCallbacks()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
replaceMessage = (index: number, message: UI_MESSAGE) => {
|
|
95
|
+
this.#messages = [...this.#messages.slice(0, index), this.snapshot(message), ...this.#messages.slice(index + 1)]
|
|
96
|
+
this.#callMessagesCallbacks()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
snapshot = <T>(value: T): T => structuredClone(value)
|
|
100
|
+
|
|
101
|
+
'~registerMessagesCallback' = (onChange: () => void, throttleWaitMs?: number): (() => void) => {
|
|
102
|
+
const callback = throttleCallback(onChange, throttleWaitMs)
|
|
103
|
+
this.#messagesCallbacks.add(callback)
|
|
104
|
+
return () => {
|
|
105
|
+
this.#messagesCallbacks.delete(callback)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
'~registerStatusCallback' = (onChange: () => void): (() => void) => {
|
|
110
|
+
this.#statusCallbacks.add(onChange)
|
|
111
|
+
return () => {
|
|
112
|
+
this.#statusCallbacks.delete(onChange)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
'~registerErrorCallback' = (onChange: () => void): (() => void) => {
|
|
117
|
+
this.#errorCallbacks.add(onChange)
|
|
118
|
+
return () => {
|
|
119
|
+
this.#errorCallbacks.delete(onChange)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#callMessagesCallbacks = () => {
|
|
124
|
+
this.#messagesCallbacks.forEach((callback) => callback())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#callStatusCallbacks = () => {
|
|
128
|
+
this.#statusCallbacks.forEach((callback) => callback())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#callErrorCallbacks = () => {
|
|
132
|
+
this.#errorCallbacks.forEach((callback) => callback())
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class SnapshottingChat<UI_MESSAGE extends UIMessage> extends AbstractChat<UI_MESSAGE> {
|
|
137
|
+
#state: SnapshottingChatState<UI_MESSAGE>
|
|
138
|
+
|
|
139
|
+
constructor({ messages, ...init }: ChatInit<UI_MESSAGE>) {
|
|
140
|
+
const state = new SnapshottingChatState(messages)
|
|
141
|
+
super({ ...init, state })
|
|
142
|
+
this.#state = state
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
'~registerMessagesCallback' = (onChange: () => void, throttleWaitMs?: number): (() => void) =>
|
|
146
|
+
this.#state['~registerMessagesCallback'](onChange, throttleWaitMs)
|
|
147
|
+
|
|
148
|
+
'~registerStatusCallback' = (onChange: () => void): (() => void) => this.#state['~registerStatusCallback'](onChange)
|
|
149
|
+
|
|
150
|
+
'~registerErrorCallback' = (onChange: () => void): (() => void) => this.#state['~registerErrorCallback'](onChange)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type UseSnapshottingChatOptions<UI_MESSAGE extends UIMessage> = ChatInit<UI_MESSAGE> & {
|
|
154
|
+
experimental_throttle?: number
|
|
155
|
+
resume?: boolean
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function useSnapshottingChat<UI_MESSAGE extends UIMessage = UIMessage>({
|
|
159
|
+
experimental_throttle: throttleWaitMs,
|
|
160
|
+
resume = false,
|
|
161
|
+
...options
|
|
162
|
+
}: UseSnapshottingChatOptions<UI_MESSAGE> = {}): UseChatHelpers<UI_MESSAGE> {
|
|
163
|
+
const callbacksRef = useRef({
|
|
164
|
+
onToolCall: options.onToolCall,
|
|
165
|
+
onData: options.onData,
|
|
166
|
+
onFinish: options.onFinish,
|
|
167
|
+
onError: options.onError,
|
|
168
|
+
sendAutomaticallyWhen: options.sendAutomaticallyWhen,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
callbacksRef.current = {
|
|
172
|
+
onToolCall: options.onToolCall,
|
|
173
|
+
onData: options.onData,
|
|
174
|
+
onFinish: options.onFinish,
|
|
175
|
+
onError: options.onError,
|
|
176
|
+
sendAutomaticallyWhen: options.sendAutomaticallyWhen,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const optionsWithCallbacks: ChatInit<UI_MESSAGE> = {
|
|
180
|
+
...options,
|
|
181
|
+
onToolCall: (arg) => callbacksRef.current.onToolCall?.(arg),
|
|
182
|
+
onData: (arg) => callbacksRef.current.onData?.(arg),
|
|
183
|
+
onFinish: (arg) => callbacksRef.current.onFinish?.(arg),
|
|
184
|
+
onError: (arg) => callbacksRef.current.onError?.(arg),
|
|
185
|
+
sendAutomaticallyWhen: (arg) => callbacksRef.current.sendAutomaticallyWhen?.(arg) ?? false,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const chatRef = useRef<SnapshottingChat<UI_MESSAGE>>(new SnapshottingChat(optionsWithCallbacks))
|
|
189
|
+
|
|
190
|
+
if (chatRef.current.id !== (options.id ?? chatRef.current.id)) {
|
|
191
|
+
chatRef.current = new SnapshottingChat(optionsWithCallbacks)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const subscribeToMessages = useCallback(
|
|
195
|
+
(update: () => void) => chatRef.current['~registerMessagesCallback'](update, throttleWaitMs),
|
|
196
|
+
[throttleWaitMs, chatRef.current.id],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const messages = useSyncExternalStore(
|
|
200
|
+
subscribeToMessages,
|
|
201
|
+
() => chatRef.current.messages,
|
|
202
|
+
() => chatRef.current.messages,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const status = useSyncExternalStore(
|
|
206
|
+
chatRef.current['~registerStatusCallback'],
|
|
207
|
+
() => chatRef.current.status,
|
|
208
|
+
() => chatRef.current.status,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const error = useSyncExternalStore(
|
|
212
|
+
chatRef.current['~registerErrorCallback'],
|
|
213
|
+
() => chatRef.current.error,
|
|
214
|
+
() => chatRef.current.error,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const setMessages = useCallback(
|
|
218
|
+
(messagesParam: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[])) => {
|
|
219
|
+
if (typeof messagesParam === 'function') {
|
|
220
|
+
messagesParam = messagesParam(chatRef.current.messages)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
chatRef.current.messages = messagesParam
|
|
224
|
+
},
|
|
225
|
+
[],
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (resume) {
|
|
230
|
+
chatRef.current.resumeStream()
|
|
231
|
+
}
|
|
232
|
+
}, [resume])
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
id: chatRef.current.id,
|
|
236
|
+
messages,
|
|
237
|
+
setMessages,
|
|
238
|
+
sendMessage: chatRef.current.sendMessage,
|
|
239
|
+
regenerate: chatRef.current.regenerate,
|
|
240
|
+
clearError: chatRef.current.clearError,
|
|
241
|
+
stop: chatRef.current.stop,
|
|
242
|
+
error,
|
|
243
|
+
resumeStream: chatRef.current.resumeStream,
|
|
244
|
+
status,
|
|
245
|
+
addToolResult: chatRef.current.addToolOutput,
|
|
246
|
+
addToolOutput: chatRef.current.addToolOutput,
|
|
247
|
+
addToolApprovalResponse: chatRef.current.addToolApprovalResponse,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useChat } from '@ai-sdk/react'
|
|
2
1
|
import type { UseChatHelpers } from '@ai-sdk/react'
|
|
3
2
|
import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'
|
|
4
3
|
import type { ChatInit, UIMessage } from 'ai'
|
|
5
4
|
import { useCallback, useEffect, useRef } from 'react'
|
|
6
5
|
|
|
6
|
+
import { useSnapshottingChat } from './use-snapshotting-chat'
|
|
7
|
+
|
|
7
8
|
type ChatTransport<UI_MESSAGE extends UIMessage> = NonNullable<ChatInit<UI_MESSAGE>['transport']>
|
|
8
9
|
|
|
9
10
|
export interface UseThreadChatOptions<UI_MESSAGE extends UIMessage> {
|
|
@@ -87,7 +88,7 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
87
88
|
setMessages,
|
|
88
89
|
addToolApprovalResponse,
|
|
89
90
|
resumeStream,
|
|
90
|
-
} =
|
|
91
|
+
} = useSnapshottingChat<UI_MESSAGE>({
|
|
91
92
|
id: chatId,
|
|
92
93
|
transport,
|
|
93
94
|
messages: initialMessages,
|