@lota-sdk/ui 0.3.0 → 0.3.1
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-thread-chat.ts +56 -5
- 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.1",
|
|
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.1",
|
|
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,
|
|
@@ -58,6 +58,9 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
58
58
|
const onDataRef = useRef(onData)
|
|
59
59
|
const onMessagesSettledRef = useRef(onMessagesSettled)
|
|
60
60
|
const setThreadRunningStateRef = useRef(setThreadRunningState)
|
|
61
|
+
const attemptedResumeForChatRef = useRef<string | null>(null)
|
|
62
|
+
const skipNextResumeForChatRef = useRef<string | null>(null)
|
|
63
|
+
const locallyStartedChatRef = useRef<string | null>(null)
|
|
61
64
|
|
|
62
65
|
useEffect(() => {
|
|
63
66
|
onDataRef.current = onData
|
|
@@ -78,8 +81,8 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
78
81
|
status,
|
|
79
82
|
error,
|
|
80
83
|
clearError,
|
|
81
|
-
sendMessage,
|
|
82
|
-
regenerate,
|
|
84
|
+
sendMessage: sendChatMessage,
|
|
85
|
+
regenerate: regenerateChat,
|
|
83
86
|
stop: stopChatStream,
|
|
84
87
|
setMessages,
|
|
85
88
|
addToolApprovalResponse,
|
|
@@ -98,10 +101,17 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
98
101
|
experimental_throttle: experimentalThrottle,
|
|
99
102
|
sendAutomaticallyWhen,
|
|
100
103
|
onFinish: () => {
|
|
104
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
105
|
+
skipNextResumeForChatRef.current = chatId
|
|
106
|
+
locallyStartedChatRef.current = null
|
|
107
|
+
}
|
|
101
108
|
setThreadRunningStateRef.current?.(false)
|
|
102
109
|
onMessagesSettledRef.current?.()
|
|
103
110
|
},
|
|
104
111
|
onError: (error) => {
|
|
112
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
113
|
+
locallyStartedChatRef.current = null
|
|
114
|
+
}
|
|
105
115
|
if (!(error instanceof Error && error.name === 'AbortError')) {
|
|
106
116
|
notifyError?.('Chat stream failed.', toError(error))
|
|
107
117
|
}
|
|
@@ -113,13 +123,54 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
|
|
|
113
123
|
// Manual resume with ref guard — prevents React StrictMode double-fire.
|
|
114
124
|
// We do NOT pass `resume` to useChat because the AI SDK's built-in effect
|
|
115
125
|
// has no StrictMode guard, causing two concurrent GET streams.
|
|
116
|
-
const resumedForChatRef = useRef<string | null>(null)
|
|
117
126
|
useEffect(() => {
|
|
118
|
-
if (!resume || status !== 'ready'
|
|
119
|
-
|
|
127
|
+
if (!resume || status !== 'ready') return
|
|
128
|
+
if (skipNextResumeForChatRef.current === chatId) {
|
|
129
|
+
skipNextResumeForChatRef.current = null
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (attemptedResumeForChatRef.current === chatId) return
|
|
133
|
+
attemptedResumeForChatRef.current = chatId
|
|
120
134
|
void resumeStream()
|
|
121
135
|
}, [resume, status, chatId, resumeStream])
|
|
122
136
|
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (status === 'ready' && resume) return
|
|
139
|
+
if (attemptedResumeForChatRef.current === chatId) {
|
|
140
|
+
attemptedResumeForChatRef.current = null
|
|
141
|
+
}
|
|
142
|
+
}, [chatId, resume, status])
|
|
143
|
+
|
|
144
|
+
const sendMessage = useCallback<UseChatHelpers<UI_MESSAGE>['sendMessage']>(
|
|
145
|
+
async (message, options) => {
|
|
146
|
+
locallyStartedChatRef.current = chatId
|
|
147
|
+
try {
|
|
148
|
+
return await sendChatMessage(message, options)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
151
|
+
locallyStartedChatRef.current = null
|
|
152
|
+
}
|
|
153
|
+
throw error
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[chatId, sendChatMessage],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const regenerate = useCallback<UseChatHelpers<UI_MESSAGE>['regenerate']>(
|
|
160
|
+
async (options) => {
|
|
161
|
+
locallyStartedChatRef.current = chatId
|
|
162
|
+
try {
|
|
163
|
+
return await regenerateChat(options)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (locallyStartedChatRef.current === chatId) {
|
|
166
|
+
locallyStartedChatRef.current = null
|
|
167
|
+
}
|
|
168
|
+
throw error
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[chatId, regenerateChat],
|
|
172
|
+
)
|
|
173
|
+
|
|
123
174
|
const stop = useCallback(async () => {
|
|
124
175
|
const stopRequest = threadId && stopRemoteRun ? stopRemoteRun(threadId) : Promise.resolve(null)
|
|
125
176
|
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
|
-
}
|