@lota-sdk/ui 0.3.0 → 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 +3 -3
- package/src/chat/attachments.ts +8 -0
- package/src/chat/index.ts +0 -1
- package/src/runtime/active-thread-helpers.ts +3 -3
- package/src/runtime/index.ts +0 -1
- package/src/runtime/use-active-thread-session.ts +4 -4
- package/src/runtime/use-snapshotting-chat.ts +249 -0
- package/src/runtime/use-thread-chat.ts +59 -7
- package/src/runtime/use-thread-management.ts +4 -2
- package/src/chat/transport.ts +0 -8
- package/src/runtime/use-lota-chat.ts +0 -13
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",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"lint": "bunx oxlint --fix -c ../oxlint.config.ts src",
|
|
19
19
|
"format": "bunx oxfmt src",
|
|
20
20
|
"typecheck": "bunx tsgo --noEmit",
|
|
21
|
-
"test:unit": "bun
|
|
21
|
+
"test:unit": "bun run scripts/test-unit.ts"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public",
|
|
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": {
|
package/src/chat/attachments.ts
CHANGED
|
@@ -14,7 +14,15 @@ type UploadedAttachmentPayload = {
|
|
|
14
14
|
url: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function hasLocalFile(value: FileUIPart): value is FileUIPart & { file: File } {
|
|
18
|
+
return 'file' in value && value.file instanceof File
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
async function toUploadFile(filePart: FileUIPart): Promise<File> {
|
|
22
|
+
if (hasLocalFile(filePart)) {
|
|
23
|
+
return filePart.file
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
if (!filePart.url) {
|
|
19
27
|
throw new Error('Attachment payload is missing file data.')
|
|
20
28
|
}
|
package/src/chat/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UIMessage } from 'ai'
|
|
2
2
|
|
|
3
|
-
export type ComposerMessage<TFile
|
|
3
|
+
export type ComposerMessage<TFile = File> = { text: string; files?: TFile[] }
|
|
4
4
|
|
|
5
|
-
export function validateComposerMessage<TFile
|
|
5
|
+
export function validateComposerMessage<TFile = File>(
|
|
6
6
|
message: ComposerMessage<TFile>,
|
|
7
7
|
): { textContent: string; files: TFile[] } | null {
|
|
8
8
|
const textContent = message.text.trim()
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from './active-thread-helpers'
|
|
2
2
|
export * from './session-common'
|
|
3
3
|
export * from './use-active-thread-session'
|
|
4
|
-
export * from './use-lota-chat'
|
|
5
4
|
export * from './use-session-messages'
|
|
6
5
|
export * from './use-thread-chat'
|
|
7
6
|
export * from './use-thread-management'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMessageCreatedAt } from '@lota-sdk/shared'
|
|
2
|
-
import type {
|
|
2
|
+
import type { UIMessage } from 'ai'
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
4
4
|
|
|
5
5
|
import { mergeChatMessages } from '../chat/messages'
|
|
@@ -21,7 +21,7 @@ interface ActiveThreadLike {
|
|
|
21
21
|
export interface UseActiveThreadSessionOptions<
|
|
22
22
|
TMessage extends UIMessage & { metadata?: { createdAt?: unknown } },
|
|
23
23
|
TThread extends ActiveThreadLike,
|
|
24
|
-
TFile
|
|
24
|
+
TFile = File,
|
|
25
25
|
TTrackerState = never,
|
|
26
26
|
> {
|
|
27
27
|
threadId: string | null
|
|
@@ -49,7 +49,7 @@ export interface UseActiveThreadSessionOptions<
|
|
|
49
49
|
isThreadBusy?: boolean
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export interface UseActiveThreadSessionReturn<TMessage extends UIMessage, TFile
|
|
52
|
+
export interface UseActiveThreadSessionReturn<TMessage extends UIMessage, TFile = File> {
|
|
53
53
|
messages: TMessage[]
|
|
54
54
|
status: ChatStatus
|
|
55
55
|
error: Error | undefined
|
|
@@ -67,7 +67,7 @@ export interface UseActiveThreadSessionReturn<TMessage extends UIMessage, TFile
|
|
|
67
67
|
export function useActiveThreadSession<
|
|
68
68
|
TMessage extends UIMessage & { metadata?: { createdAt?: unknown } },
|
|
69
69
|
TThread extends ActiveThreadLike,
|
|
70
|
-
TFile
|
|
70
|
+
TFile = File,
|
|
71
71
|
TTrackerState = never,
|
|
72
72
|
>({
|
|
73
73
|
threadId,
|
|
@@ -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> {
|
|
@@ -58,6 +59,9 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
58
59
|
const onDataRef = useRef(onData)
|
|
59
60
|
const onMessagesSettledRef = useRef(onMessagesSettled)
|
|
60
61
|
const setThreadRunningStateRef = useRef(setThreadRunningState)
|
|
62
|
+
const attemptedResumeForChatRef = useRef<string | null>(null)
|
|
63
|
+
const skipNextResumeForChatRef = useRef<string | null>(null)
|
|
64
|
+
const locallyStartedChatRef = useRef<string | null>(null)
|
|
61
65
|
|
|
62
66
|
useEffect(() => {
|
|
63
67
|
onDataRef.current = onData
|
|
@@ -78,13 +82,13 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
78
82
|
status,
|
|
79
83
|
error,
|
|
80
84
|
clearError,
|
|
81
|
-
sendMessage,
|
|
82
|
-
regenerate,
|
|
85
|
+
sendMessage: sendChatMessage,
|
|
86
|
+
regenerate: regenerateChat,
|
|
83
87
|
stop: stopChatStream,
|
|
84
88
|
setMessages,
|
|
85
89
|
addToolApprovalResponse,
|
|
86
90
|
resumeStream,
|
|
87
|
-
} =
|
|
91
|
+
} = useSnapshottingChat<UI_MESSAGE>({
|
|
88
92
|
id: chatId,
|
|
89
93
|
transport,
|
|
90
94
|
messages: initialMessages,
|
|
@@ -98,10 +102,17 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
98
102
|
experimental_throttle: experimentalThrottle,
|
|
99
103
|
sendAutomaticallyWhen,
|
|
100
104
|
onFinish: () => {
|
|
105
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
106
|
+
skipNextResumeForChatRef.current = chatId
|
|
107
|
+
locallyStartedChatRef.current = null
|
|
108
|
+
}
|
|
101
109
|
setThreadRunningStateRef.current?.(false)
|
|
102
110
|
onMessagesSettledRef.current?.()
|
|
103
111
|
},
|
|
104
112
|
onError: (error) => {
|
|
113
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
114
|
+
locallyStartedChatRef.current = null
|
|
115
|
+
}
|
|
105
116
|
if (!(error instanceof Error && error.name === 'AbortError')) {
|
|
106
117
|
notifyError?.('Chat stream failed.', toError(error))
|
|
107
118
|
}
|
|
@@ -113,13 +124,54 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
113
124
|
// Manual resume with ref guard — prevents React StrictMode double-fire.
|
|
114
125
|
// We do NOT pass `resume` to useChat because the AI SDK's built-in effect
|
|
115
126
|
// has no StrictMode guard, causing two concurrent GET streams.
|
|
116
|
-
const resumedForChatRef = useRef<string | null>(null)
|
|
117
127
|
useEffect(() => {
|
|
118
|
-
if (!resume || status !== 'ready'
|
|
119
|
-
|
|
128
|
+
if (!resume || status !== 'ready') return
|
|
129
|
+
if (skipNextResumeForChatRef.current === chatId) {
|
|
130
|
+
skipNextResumeForChatRef.current = null
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (attemptedResumeForChatRef.current === chatId) return
|
|
134
|
+
attemptedResumeForChatRef.current = chatId
|
|
120
135
|
void resumeStream()
|
|
121
136
|
}, [resume, status, chatId, resumeStream])
|
|
122
137
|
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (status === 'ready' && resume) return
|
|
140
|
+
if (attemptedResumeForChatRef.current === chatId) {
|
|
141
|
+
attemptedResumeForChatRef.current = null
|
|
142
|
+
}
|
|
143
|
+
}, [chatId, resume, status])
|
|
144
|
+
|
|
145
|
+
const sendMessage = useCallback<UseChatHelpers<UI_MESSAGE>['sendMessage']>(
|
|
146
|
+
async (message, options) => {
|
|
147
|
+
locallyStartedChatRef.current = chatId
|
|
148
|
+
try {
|
|
149
|
+
return await sendChatMessage(message, options)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
152
|
+
locallyStartedChatRef.current = null
|
|
153
|
+
}
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[chatId, sendChatMessage],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const regenerate = useCallback<UseChatHelpers<UI_MESSAGE>['regenerate']>(
|
|
161
|
+
async (options) => {
|
|
162
|
+
locallyStartedChatRef.current = chatId
|
|
163
|
+
try {
|
|
164
|
+
return await regenerateChat(options)
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
167
|
+
locallyStartedChatRef.current = null
|
|
168
|
+
}
|
|
169
|
+
throw error
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
[chatId, regenerateChat],
|
|
173
|
+
)
|
|
174
|
+
|
|
123
175
|
const stop = useCallback(async () => {
|
|
124
176
|
const stopRequest = threadId && stopRemoteRun ? stopRemoteRun(threadId) : Promise.resolve(null)
|
|
125
177
|
const [stopResponse] = await Promise.allSettled([stopRequest, stopChatStream()])
|
|
@@ -5,7 +5,7 @@ const PLACEHOLDER_THREAD_PREFIX = '__placeholder_'
|
|
|
5
5
|
export interface ThreadManagementItem<TAgentId extends string = string> {
|
|
6
6
|
threadId: string
|
|
7
7
|
title: string
|
|
8
|
-
status: '
|
|
8
|
+
status: 'active' | 'archived'
|
|
9
9
|
type: string
|
|
10
10
|
isMutable: boolean
|
|
11
11
|
agentId?: TAgentId | null
|
|
@@ -62,7 +62,7 @@ export function findReusableRegularGroupThreadId<TThread extends ThreadManagemen
|
|
|
62
62
|
defaultThreadTitle: string,
|
|
63
63
|
): string | null {
|
|
64
64
|
const draft = threads.find(
|
|
65
|
-
(thread) => thread.type === 'group' && thread.status === '
|
|
65
|
+
(thread) => thread.type === 'group' && thread.status === 'active' && thread.title.trim() === defaultThreadTitle,
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
return draft?.threadId ?? null
|
|
@@ -245,6 +245,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
245
245
|
return
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
if (!enabled) return
|
|
248
249
|
if (isCreatingThreadRef.current) return
|
|
249
250
|
if (isCreatingThread) return
|
|
250
251
|
isCreatingThreadRef.current = true
|
|
@@ -257,6 +258,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
257
258
|
isCreatingThreadRef.current = false
|
|
258
259
|
})
|
|
259
260
|
}, [
|
|
261
|
+
enabled,
|
|
260
262
|
currentThreadId,
|
|
261
263
|
handleCreateNewThread,
|
|
262
264
|
isCreatingThread,
|
package/src/chat/transport.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { DefaultChatTransport } from 'ai'
|
|
2
|
-
import type { HttpChatTransportInitOptions, UIMessage } from 'ai'
|
|
3
|
-
|
|
4
|
-
export type ChatTransportOptions<UI_MESSAGE extends UIMessage> = HttpChatTransportInitOptions<UI_MESSAGE>
|
|
5
|
-
|
|
6
|
-
export function createChatTransport<UI_MESSAGE extends UIMessage>(options: ChatTransportOptions<UI_MESSAGE>) {
|
|
7
|
-
return new DefaultChatTransport<UI_MESSAGE>(options)
|
|
8
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { UIMessage } from 'ai'
|
|
2
|
-
|
|
3
|
-
import type { UseThreadChatOptions, UseThreadChatReturn } from './use-thread-chat'
|
|
4
|
-
import { useThreadChat } from './use-thread-chat'
|
|
5
|
-
|
|
6
|
-
export type UseLotaChatOptions<UI_MESSAGE extends UIMessage> = UseThreadChatOptions<UI_MESSAGE>
|
|
7
|
-
export type UseLotaChatReturn<UI_MESSAGE extends UIMessage> = UseThreadChatReturn<UI_MESSAGE>
|
|
8
|
-
|
|
9
|
-
export function useLotaChat<UI_MESSAGE extends UIMessage>(
|
|
10
|
-
options: UseLotaChatOptions<UI_MESSAGE>,
|
|
11
|
-
): UseLotaChatReturn<UI_MESSAGE> {
|
|
12
|
-
return useThreadChat(options)
|
|
13
|
-
}
|