Skip to content

Commit 218dcd1

Browse files
authored
Reactive session (#728)
* Improve rate limiting to match multiple chat endpoints - Use string suffix matching for chat endpoints instead of exact path matching - Add support for /chatbot endpoint in rate limiting - Reorganize imports for better readability * Fix session handling and add session change detection - Convert sessionUuid to ref and add watcher for session changes - Stop active streams when switching sessions - Add session validation in composables to prevent errors with empty sessions - Improve error handling for session synchronization * Add auth initialization wait with timeout in axios interceptor - Implement `waitForInitialization` method in auth store using Vue watcher - Replace busy-wait loop with promise-based approach in axios interceptor - Add timeout handling (default 10s) to prevent hanging requests - Maintain existing auth state management while improving performance
1 parent 61564d5 commit 218dcd1

File tree

6 files changed

+136
-23
lines changed

6 files changed

+136
-23
lines changed

web/src/store/modules/auth/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineStore } from 'pinia'
2+
import { watch } from 'vue'
23
import { getExpiresIn, getToken, removeExpiresIn, removeToken, setExpiresIn, setToken } from './helper'
34

45
let activeRefreshPromise: Promise<string | void> | null = null
@@ -125,5 +126,30 @@ export const useAuthStore = defineStore('auth-store', {
125126
this.expiresIn = null
126127
removeExpiresIn()
127128
},
129+
async waitForInitialization(timeoutMs = 10000) {
130+
if (!this.isInitializing) {
131+
return
132+
}
133+
134+
await new Promise<void>((resolve) => {
135+
let stopWatcher: (() => void) | null = null
136+
const timeoutId = setTimeout(() => {
137+
if (stopWatcher) stopWatcher()
138+
resolve()
139+
}, timeoutMs)
140+
141+
stopWatcher = watch(
142+
() => this.isInitializing,
143+
(isInit) => {
144+
if (!isInit) {
145+
clearTimeout(timeoutId)
146+
if (stopWatcher) stopWatcher()
147+
resolve()
148+
}
149+
},
150+
{ immediate: false }
151+
)
152+
})
153+
},
128154
},
129155
})

web/src/utils/request/axios.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ service.interceptors.request.use(
2020
// Wait for auth initialization to complete before making API calls
2121
if (authStore.isInitializing) {
2222
logger.debug('Waiting for auth initialization to complete', 'Axios', { url: config.url })
23-
// Wait for initialization to complete
24-
while (authStore.isInitializing) {
25-
await new Promise(resolve => setTimeout(resolve, 50))
23+
await authStore.waitForInitialization()
24+
25+
if (authStore.isInitializing) {
26+
logger.warn('Auth initialization still in progress after timeout', 'Axios', { url: config.url })
27+
} else {
28+
logger.debug('Auth initialization completed', 'Axios', { url: config.url })
2629
}
27-
logger.debug('Auth initialization completed', 'Axios', { url: config.url })
2830
}
2931

3032
// Check if token is expired before making request

web/src/views/chat/components/Conversation.vue

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang='ts' setup>
2-
import { computed, onMounted, onUnmounted, ref } from 'vue'
2+
import { computed, onMounted, onUnmounted, ref, toRef, watch } from 'vue'
33
import { NAutoComplete, NButton, NInput, NModal, NSpin } from 'naive-ui'
44
import { v7 as uuidv7 } from 'uuid'
55
import { useScroll } from '@/views/chat/hooks/useScroll'
@@ -36,13 +36,15 @@ const messageStore = useMessageStore()
3636
const sessionStore = useSessionStore()
3737
const promptStore = usePromptStore()
3838
39-
const { sessionUuid } = defineProps({
39+
const props = defineProps({
4040
sessionUuid: {
4141
type: String,
4242
required: false,
4343
default: ''
4444
},
45-
});
45+
})
46+
47+
const sessionUuid = toRef(props, 'sessionUuid')
4648
4749
const { isMobile } = useBasicLayout()
4850
const { scrollRef, scrollToBottom, smoothScrollToBottomIfAtBottom } = useScroll()
@@ -53,13 +55,25 @@ const regenerate = useRegenerate(sessionUuid)
5355
const searchAndPrompts = useSearchAndPrompts()
5456
const chatActions = useChatActions(sessionUuid)
5557
56-
// Sync chat messages only if we have a valid sessionUuid
57-
if (sessionUuid) {
58-
messageStore.syncChatMessages(sessionUuid)
59-
}
58+
watch(sessionUuid, async (newSession, oldSession) => {
59+
if (!newSession) {
60+
return
61+
}
62+
63+
if (oldSession && oldSession !== newSession) {
64+
conversationFlow.stopStream()
65+
regenerate.stopRegenerate()
66+
}
67+
68+
try {
69+
await messageStore.syncChatMessages(newSession)
70+
} catch (error) {
71+
console.error('Failed to sync messages for session change:', error)
72+
}
73+
}, { immediate: true })
6074
61-
const dataSources = computed(() => messageStore.getChatSessionDataByUuid(sessionUuid))
62-
const chatSession = computed(() => sessionStore.getChatSessionByUuid(sessionUuid))
75+
const dataSources = computed(() => messageStore.getChatSessionDataByUuid(sessionUuid.value))
76+
const chatSession = computed(() => sessionStore.getChatSessionByUuid(sessionUuid.value))
6377
6478
// Destructure from composables
6579
const { prompt, searchOptions, renderOption, handleSelectAutoComplete, handleUsePrompt } = searchAndPrompts
@@ -353,4 +367,4 @@ function handleUseQuestion(question: string) {
353367
width: 4px;
354368
}
355369
}
356-
</style>
370+
</style>

web/src/views/chat/composables/useChatActions.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ref } from 'vue'
1+
import { ref, type Ref } from 'vue'
22
import { useDialog, useMessage } from 'naive-ui'
33
import { v4 as uuidv4 } from 'uuid'
44
import { createChatBot, createChatSnapshot, getChatSessionDefault } from '@/api'
@@ -9,7 +9,7 @@ import { nowISO } from '@/utils/date'
99
import { extractArtifacts } from '@/utils/artifacts'
1010
import { t } from '@/locales'
1111

12-
export function useChatActions(sessionUuid: string) {
12+
export function useChatActions(sessionUuidRef: Ref<string>) {
1313
const dialog = useDialog()
1414
const nui_msg = useMessage()
1515
const sessionStore = useSessionStore()
@@ -40,6 +40,12 @@ export function useChatActions(sessionUuid: string) {
4040
}
4141

4242
async function handleSnapshot() {
43+
const sessionUuid = sessionUuidRef.value
44+
if (!sessionUuid) {
45+
nui_msg.error('No active session selected.')
46+
return
47+
}
48+
4349
snapshotLoading.value = true
4450
try {
4551
const snapshot = await createChatSnapshot(sessionUuid)
@@ -54,6 +60,12 @@ export function useChatActions(sessionUuid: string) {
5460
}
5561

5662
async function handleCreateBot() {
63+
const sessionUuid = sessionUuidRef.value
64+
if (!sessionUuid) {
65+
nui_msg.error('No active session selected.')
66+
return
67+
}
68+
5769
botLoading.value = true
5870
try {
5971
const snapshot = await createChatBot(sessionUuid)
@@ -71,6 +83,12 @@ export function useChatActions(sessionUuid: string) {
7183
if (loading.value)
7284
return
7385

86+
const sessionUuid = sessionUuidRef.value
87+
if (!sessionUuid) {
88+
nui_msg.error('No active session selected.')
89+
return
90+
}
91+
7492
console.log('🔄 handleClear called with sessionUuid:', sessionUuid)
7593

7694
dialog.warning({
@@ -94,6 +112,12 @@ export function useChatActions(sessionUuid: string) {
94112
}
95113

96114
const handleCodeExampleAdded = async (codeInfo: any, streamResponse: any) => {
115+
const sessionUuid = sessionUuidRef.value
116+
if (!sessionUuid) {
117+
nui_msg.error('No active session selected.')
118+
return
119+
}
120+
97121
const exampleMessage = `📁 **Files uploaded successfully!**
98122
99123
**Python example:**
@@ -144,4 +168,4 @@ Your files are now available in the Virtual File System! 🚀`
144168
handleVFSFileUploaded,
145169
handleCodeExampleAdded
146170
}
147-
}
171+
}

web/src/views/chat/composables/useConversationFlow.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ref } from 'vue'
1+
import { ref, type Ref } from 'vue'
22
import { v7 as uuidv7 } from 'uuid'
33
import { nowISO } from '@/utils/date'
44
import { useChat } from '@/views/chat/hooks/useChat'
@@ -18,7 +18,7 @@ interface ChatMessage {
1818
}
1919

2020
export function useConversationFlow(
21-
sessionUuid: string,
21+
sessionUuidRef: Ref<string>,
2222
scrollToBottom: () => Promise<void>,
2323
smoothScrollToBottomIfAtBottom: () => Promise<void>
2424
) {
@@ -36,6 +36,11 @@ export function useConversationFlow(
3636
return false
3737
}
3838

39+
if (!sessionUuidRef.value) {
40+
showErrorNotification('No active session selected')
41+
return false
42+
}
43+
3944
// Validate message content
4045
const messageValidation = validateChatMessage(message)
4146
if (!messageValidation.isValid) {
@@ -47,6 +52,11 @@ export function useConversationFlow(
4752
}
4853

4954
async function addUserMessage(chatUuid: string, message: string): Promise<void> {
55+
const sessionUuid = sessionUuidRef.value
56+
if (!sessionUuid) {
57+
return
58+
}
59+
5060
const chatMessage: ChatMessage = {
5161
uuid: chatUuid,
5262
dateTime: nowISO(),
@@ -60,6 +70,11 @@ export function useConversationFlow(
6070
}
6171

6272
async function initializeChatResponse(dataSources: any[]): Promise<number> {
73+
const sessionUuid = sessionUuidRef.value
74+
if (!sessionUuid) {
75+
return dataSources.length - 1
76+
}
77+
6378
addChat(sessionUuid, {
6479
uuid: '',
6580
dateTime: nowISO(),
@@ -76,6 +91,11 @@ export function useConversationFlow(
7691
handleApiError(error, 'conversation-stream')
7792

7893
const lastMessage = dataSources[responseIndex]
94+
const sessionUuid = sessionUuidRef.value
95+
if (!sessionUuid) {
96+
return
97+
}
98+
7999
if (lastMessage) {
80100
const errorMessage: ChatMessage = {
81101
uuid: lastMessage.uuid || uuidv7(),
@@ -103,6 +123,13 @@ export function useConversationFlow(
103123
dataSources: any[],
104124
chatUuid: string
105125
): Promise<void> {
126+
const sessionUuid = sessionUuidRef.value
127+
if (!sessionUuid) {
128+
loading.value = false
129+
abortController.value = null
130+
return
131+
}
132+
106133
loading.value = true
107134
abortController.value = new AbortController()
108135
const responseIndex = await initializeChatResponse(dataSources)
@@ -148,4 +175,4 @@ export function useConversationFlow(
148175
startStream,
149176
stopStream
150177
}
151-
}
178+
}

web/src/views/chat/composables/useRegenerate.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { ref } from 'vue'
1+
import { ref, type Ref } from 'vue'
22
import { deleteChatMessage } from '@/api'
33
import { nowISO } from '@/utils/date'
44
import { useChat } from '@/views/chat/hooks/useChat'
55
import { useStreamHandling } from './useStreamHandling'
66
import { t } from '@/locales'
77

8-
export function useRegenerate(sessionUuid: string) {
8+
export function useRegenerate(sessionUuidRef: Ref<string>) {
99
const loading = ref<boolean>(false)
1010
const abortController = ref<AbortController | null>(null)
1111
const { addChat, updateChat, updateChatPartial } = useChat()
@@ -21,6 +21,11 @@ export function useRegenerate(sessionUuid: string) {
2121
chat: any,
2222
dataSources: any[]
2323
): Promise<{ updateIndex: number; isRegenerate: boolean }> {
24+
const sessionUuid = sessionUuidRef.value
25+
if (!sessionUuid) {
26+
return { updateIndex: index, isRegenerate: true }
27+
}
28+
2429
loading.value = true
2530

2631
let updateIndex = index
@@ -49,6 +54,11 @@ export function useRegenerate(sessionUuid: string) {
4954
index: number,
5055
dataSources: any[]
5156
): Promise<{ updateIndex: number; isRegenerate: boolean }> {
57+
const sessionUuid = sessionUuidRef.value
58+
if (!sessionUuid) {
59+
return { updateIndex: index, isRegenerate: false }
60+
}
61+
5262
const chatNext = dataSources[index + 1]
5363
let updateIndex = index + 1
5464
const isRegenerate = false
@@ -80,6 +90,11 @@ export function useRegenerate(sessionUuid: string) {
8090
}
8191

8292
function handleRegenerateError(error: any, chatUuid: string, index: number): void {
93+
const sessionUuid = sessionUuidRef.value
94+
if (!sessionUuid) {
95+
return
96+
}
97+
8398
console.error('Regenerate error:', error)
8499

85100
if (error.message === 'canceled') {
@@ -112,6 +127,11 @@ export function useRegenerate(sessionUuid: string) {
112127
async function onRegenerate(index: number, dataSources: any[]): Promise<void> {
113128
if (!validateRegenerateInput()) return
114129

130+
const sessionUuid = sessionUuidRef.value
131+
if (!sessionUuid) {
132+
return
133+
}
134+
115135
const chat = dataSources[index]
116136
abortController.value = new AbortController()
117137
const { updateIndex, isRegenerate } = await prepareRegenerateContext(index, chat, dataSources)
@@ -148,4 +168,4 @@ export function useRegenerate(sessionUuid: string) {
148168
onRegenerate,
149169
stopRegenerate
150170
}
151-
}
171+
}

0 commit comments

Comments
 (0)