fix(chat): serialize sends and reset on cleanup (#827)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatHistoryMessage } from '@proj-airi/stage-ui/components'
|
||||
import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { ChatHistory } from '@proj-airi/stage-ui/components'
|
||||
@@ -9,7 +10,7 @@ import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { BasicTextarea } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { widgetsTools } from '../stores/tools/builtin/widgets'
|
||||
@@ -37,10 +38,16 @@ async function handleSend() {
|
||||
return
|
||||
}
|
||||
|
||||
const textToSend = messageInput.value
|
||||
const attachmentsToSend = attachments.value.map(att => ({ ...att }))
|
||||
|
||||
// optimistic clear
|
||||
messageInput.value = ''
|
||||
attachments.value = []
|
||||
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
const attachmentsToSend = attachments.value.map(({ data, mimeType, type }) => ({ data, mimeType, type }))
|
||||
await send(messageInput.value, {
|
||||
await send(textToSend, {
|
||||
model: activeModel.value,
|
||||
chatProvider: await providersStore.getProviderInstance<ChatProvider>(activeProvider.value),
|
||||
providerConfig,
|
||||
@@ -48,10 +55,15 @@ async function handleSend() {
|
||||
tools: widgetsTools,
|
||||
})
|
||||
|
||||
// clear after sending
|
||||
messageInput.value = ''
|
||||
attachmentsToSend.forEach(att => URL.revokeObjectURL(att.url))
|
||||
}
|
||||
catch (error) {
|
||||
// restore on failure
|
||||
messageInput.value = textToSend
|
||||
attachments.value = attachmentsToSend.map(att => ({
|
||||
...att,
|
||||
url: URL.createObjectURL(new Blob([Uint8Array.from(atob(att.data), c => c.charCodeAt(0))], { type: att.mimeType })),
|
||||
}))
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
@@ -142,13 +154,15 @@ onAfterMessageComposed(async () => {
|
||||
attachments.value.forEach(att => URL.revokeObjectURL(att.url))
|
||||
attachments.value = []
|
||||
})
|
||||
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryMessage[])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full w-full flex="~ col gap-1">
|
||||
<div w-full flex-1 overflow-hidden>
|
||||
<ChatHistory
|
||||
:messages="messages"
|
||||
:messages="historyMessages"
|
||||
:sending="sending"
|
||||
:streaming-message="streamingMessage"
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatHistoryMessage } from '@proj-airi/stage-ui/components'
|
||||
|
||||
import { ChatHistory } from '@proj-airi/stage-ui/components'
|
||||
import { useChatStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useDeferredMount } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ChatActionButtons from '../Widgets/ChatActionButtons.vue'
|
||||
import ChatArea from '../Widgets/ChatArea.vue'
|
||||
@@ -13,6 +15,7 @@ const { isReady } = useDeferredMount()
|
||||
const { messages, sending, streamingMessage } = storeToRefs(useChatStore())
|
||||
|
||||
const isLoading = ref(true)
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryMessage[])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,7 +32,7 @@ const isLoading = ref(true)
|
||||
<div w="full" max-h="<md:[60%]" py="<sm:2" flex="~ col" rounded="lg" relative h-full flex-1 overflow-hidden px="2 <md:0" py-4>
|
||||
<ChatHistory
|
||||
v-if="isReady"
|
||||
:messages="messages"
|
||||
:messages="historyMessages"
|
||||
:sending="sending"
|
||||
:streaming-message="streamingMessage"
|
||||
h-full
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatHistoryMessage } from '@proj-airi/stage-ui/components'
|
||||
import type { ChatProvider } from '@xsai-ext/shared-providers'
|
||||
|
||||
import { ChatHistory, HearingConfigDialog } from '@proj-airi/stage-ui/components'
|
||||
@@ -11,7 +12,7 @@ import { useSettings, useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/
|
||||
import { BasicTextarea, useTheme } from '@proj-airi/ui'
|
||||
import { useResizeObserver, useScreenSafeArea } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
@@ -24,7 +25,9 @@ import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
const { isDark, toggleDark } = useTheme()
|
||||
const hearingDialogOpen = ref(false)
|
||||
const { messages, sending, streamingMessage } = storeToRefs(useChatStore())
|
||||
const chatStore = useChatStore()
|
||||
const { messages, sending, streamingMessage } = storeToRefs(chatStore)
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryMessage[])
|
||||
|
||||
const viewControlsActiveMode = ref<'x' | 'y' | 'z' | 'scale'>('scale')
|
||||
const viewControlsInputsRef = useTemplateRef<InstanceType<typeof ViewControlInputs>>('viewControlsInputs')
|
||||
@@ -41,7 +44,7 @@ useResizeObserver(document.documentElement, () => screenSafeArea.update())
|
||||
const { themeColorsHueDynamic, stageViewControlsEnabled } = storeToRefs(useSettings())
|
||||
const settingsAudioDevice = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(settingsAudioDevice)
|
||||
const { send, onAfterMessageComposed, discoverToolsCompatibility, cleanupMessages } = useChatStore()
|
||||
const { send, onAfterMessageComposed, discoverToolsCompatibility, cleanupMessages } = chatStore
|
||||
const { t } = useI18n()
|
||||
const { audioContext } = useAudioContext()
|
||||
const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer()
|
||||
@@ -62,16 +65,20 @@ async function handleSend() {
|
||||
return
|
||||
}
|
||||
|
||||
const textToSend = messageInput.value
|
||||
messageInput.value = ''
|
||||
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
|
||||
await send(messageInput.value, {
|
||||
await send(textToSend, {
|
||||
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
model: activeModel.value,
|
||||
providerConfig,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
messageInput.value = textToSend
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
@@ -113,7 +120,6 @@ watch(hearingDialogOpen, (value) => {
|
||||
})
|
||||
|
||||
onAfterMessageComposed(async () => {
|
||||
messageInput.value = ''
|
||||
})
|
||||
|
||||
watch([activeProvider, activeModel], async () => {
|
||||
@@ -139,7 +145,7 @@ onMounted(() => {
|
||||
<ChatHistory
|
||||
v-if="!stageViewControlsEnabled"
|
||||
variant="mobile"
|
||||
:messages="messages"
|
||||
:messages="historyMessages"
|
||||
:sending="sending"
|
||||
:streaming-message="streamingMessage"
|
||||
max-w="[calc(100%-3.5rem)]"
|
||||
|
||||
@@ -25,7 +25,8 @@ const { themeColorsHueDynamic } = storeToRefs(useSettings())
|
||||
|
||||
const { askPermission } = useSettingsAudioDevice()
|
||||
const { enabled, selectedAudioInput, stream, audioInputs } = storeToRefs(useSettingsAudioDevice())
|
||||
const { send, onAfterMessageComposed, discoverToolsCompatibility } = useChatStore()
|
||||
const chatStore = useChatStore()
|
||||
const { send, onAfterMessageComposed, discoverToolsCompatibility } = chatStore
|
||||
const { messages } = storeToRefs(useChatStore())
|
||||
const { audioContext } = useAudioContext()
|
||||
const { t } = useI18n()
|
||||
@@ -35,16 +36,20 @@ async function handleSend() {
|
||||
return
|
||||
}
|
||||
|
||||
const textToSend = messageInput.value
|
||||
messageInput.value = ''
|
||||
|
||||
try {
|
||||
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
|
||||
|
||||
await send(messageInput.value, {
|
||||
await send(textToSend, {
|
||||
chatProvider: await providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
|
||||
model: activeModel.value,
|
||||
providerConfig,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
messageInput.value = textToSend
|
||||
messages.value.pop()
|
||||
messages.value.push({
|
||||
role: 'error',
|
||||
@@ -66,7 +71,6 @@ watch([activeProvider, activeModel], async () => {
|
||||
})
|
||||
|
||||
onAfterMessageComposed(async () => {
|
||||
messageInput.value = ''
|
||||
})
|
||||
|
||||
const { startAnalyzer, stopAnalyzer, volumeLevel } = useAudioAnalyzer()
|
||||
|
||||
@@ -11,7 +11,7 @@ import ChatUserItem from './ChatUserItem.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
messages: ChatHistoryMessage[]
|
||||
streamingMessage?: ChatAssistantMessage
|
||||
streamingMessage?: ChatAssistantMessage & { context?: { ts?: number } }
|
||||
sending?: boolean
|
||||
assistantLabel?: string
|
||||
userLabel?: string
|
||||
@@ -46,18 +46,33 @@ watch([() => props.messages, () => props.streamingMessage], scrollToBottom, { de
|
||||
watch(() => props.sending, scrollToBottom, { flush: 'post' })
|
||||
onMounted(scrollToBottom)
|
||||
|
||||
const streaming = computed<ChatAssistantMessage>(() => props.streamingMessage ?? { role: 'assistant', content: '', slices: [], tool_results: [] })
|
||||
const streaming = computed<ChatAssistantMessage & { context?: { ts?: number } }>(() => props.streamingMessage ?? { role: 'assistant', content: '', slices: [], tool_results: [] })
|
||||
const showStreamingPlaceholder = computed(() => (streaming.value.slices?.length ?? 0) === 0 && !streaming.value.content)
|
||||
const streamingTs = computed(() => streaming.value.context?.ts)
|
||||
const renderMessages = computed<ChatHistoryMessage[]>(() => {
|
||||
if (!props.sending)
|
||||
return props.messages
|
||||
|
||||
const streamTs = streamingTs.value
|
||||
if (!streamTs)
|
||||
return props.messages
|
||||
|
||||
const hasStreamAlready = streamTs && props.messages.some(msg => msg.context?.ts === streamTs)
|
||||
if (hasStreamAlready)
|
||||
return props.messages
|
||||
|
||||
return [...props.messages, streaming.value]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chatHistoryRef" v-auto-animate flex="~ col" relative h-full w-full overflow-y-auto rounded-xl px="<sm:2" py="<sm:2" :class="variant === 'mobile' ? 'gap-1' : 'gap-2'">
|
||||
<template v-for="(message, index) in messages" :key="index">
|
||||
<template v-for="(message, index) in renderMessages" :key="message.context?.ts ?? index">
|
||||
<div v-if="message.role === 'error'">
|
||||
<ChatErrorItem
|
||||
:message="message"
|
||||
:label="labels.error"
|
||||
:show-placeholder="sending && index === messages.length - 1"
|
||||
:show-placeholder="sending && index === renderMessages.length - 1"
|
||||
:variant="variant"
|
||||
/>
|
||||
</div>
|
||||
@@ -66,6 +81,7 @@ const showStreamingPlaceholder = computed(() => (streaming.value.slices?.length
|
||||
<ChatAssistantItem
|
||||
:message="message"
|
||||
:label="labels.assistant"
|
||||
:show-placeholder="message.context?.ts === streamingTs ? showStreamingPlaceholder : false"
|
||||
:variant="variant"
|
||||
/>
|
||||
</div>
|
||||
@@ -78,14 +94,5 @@ const showStreamingPlaceholder = computed(() => (streaming.value.slices?.length
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="sending">
|
||||
<ChatAssistantItem
|
||||
:message="streaming"
|
||||
:label="labels.assistant"
|
||||
:show-placeholder="showStreamingPlaceholder"
|
||||
:variant="variant"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,4 +7,8 @@ export interface ChatErrorMessage {
|
||||
|
||||
export type ChatHistoryMessage = (ChatMessage | ChatErrorMessage) & {
|
||||
slices?: ChatSlices[]
|
||||
context?: {
|
||||
ts?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ interface MessageContext {
|
||||
meta?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type ChatEntry = (ChatMessage | ErrorMessage) & { context?: MessageContext }
|
||||
export type { ChatEntry }
|
||||
export type ChatEntry = (ChatMessage | ErrorMessage) & { context?: MessageContext }
|
||||
|
||||
export interface ContextPayload {
|
||||
content?: unknown
|
||||
@@ -52,6 +51,8 @@ const ACTIVE_SESSION_STORAGE_KEY = 'chat/active-session'
|
||||
export const CONTEXT_CHANNEL_NAME = 'airi-context-update'
|
||||
export const CHAT_STREAM_CHANNEL_NAME = 'airi-chat-stream'
|
||||
|
||||
type StreamingAssistantMessage = ChatAssistantMessage & { context?: MessageContext }
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const { stream, discoverToolsCompatibility } = useLLM()
|
||||
const { systemPrompt } = storeToRefs(useAiriCardStore())
|
||||
@@ -60,6 +61,62 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const sessionMessages = useLocalStorage<Record<string, ChatEntry[]>>(CHAT_STORAGE_KEY, {})
|
||||
|
||||
const sending = ref(false)
|
||||
const streamingMessage = ref<StreamingAssistantMessage>({ role: 'assistant', content: '', slices: [], tool_results: [] })
|
||||
const sessionGenerations = ref<Record<string, number>>({})
|
||||
|
||||
interface SendOptions {
|
||||
model: string
|
||||
chatProvider: ChatProvider
|
||||
providerConfig?: Record<string, unknown>
|
||||
attachments?: { type: 'image', data: string, mimeType: string }[]
|
||||
tools?: StreamOptions['tools']
|
||||
}
|
||||
|
||||
interface QueuedSend {
|
||||
sendingMessage: string
|
||||
options: SendOptions
|
||||
generation: number
|
||||
sessionId: string
|
||||
cancelled?: boolean
|
||||
deferred: {
|
||||
resolve: () => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
}
|
||||
|
||||
const pendingQueuedSends = ref<QueuedSend[]>([])
|
||||
|
||||
const sendQueue = createQueue<QueuedSend>({
|
||||
handlers: [
|
||||
async ({ data }) => {
|
||||
const { sendingMessage, options, generation, deferred, sessionId, cancelled } = data
|
||||
|
||||
if (cancelled)
|
||||
return
|
||||
|
||||
if (getSessionGeneration(sessionId) !== generation) {
|
||||
deferred.reject(new Error('Chat session was reset before send could start'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await performSend(sendingMessage, options, generation, sessionId)
|
||||
deferred.resolve()
|
||||
}
|
||||
catch (error) {
|
||||
deferred.reject(error)
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
sendQueue.on('enqueue', (queuedSend) => {
|
||||
pendingQueuedSends.value = [...pendingQueuedSends.value, queuedSend]
|
||||
})
|
||||
|
||||
sendQueue.on('dequeue', (queuedSend) => {
|
||||
pendingQueuedSends.value = pendingQueuedSends.value.filter(item => item !== queuedSend)
|
||||
})
|
||||
|
||||
// ----- Hooks (UI callbacks) -----
|
||||
const onBeforeMessageComposedHooks = ref<Array<(message: string) => Promise<void>>>([])
|
||||
@@ -177,6 +234,22 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const codeBlockSystemPrompt = '- For any programming code block, always specify the programming language that supported on @shikijs/rehype on the rendered markdown, eg. ```python ... ```\n'
|
||||
const mathSyntaxSystemPrompt = '- For any math equation, use LaTeX format, eg: $ x^3 $, always escape dollar sign outside math equation\n'
|
||||
|
||||
function ensureSessionGeneration(sessionId: string) {
|
||||
if (sessionGenerations.value[sessionId] === undefined)
|
||||
sessionGenerations.value = { ...sessionGenerations.value, [sessionId]: 0 }
|
||||
}
|
||||
|
||||
function getSessionGeneration(sessionId: string) {
|
||||
ensureSessionGeneration(sessionId)
|
||||
return sessionGenerations.value[sessionId] ?? 0
|
||||
}
|
||||
|
||||
function bumpSessionGeneration(sessionId: string) {
|
||||
const nextGeneration = getSessionGeneration(sessionId) + 1
|
||||
sessionGenerations.value = { ...sessionGenerations.value, [sessionId]: nextGeneration }
|
||||
return nextGeneration
|
||||
}
|
||||
|
||||
function generateInitialMessage() {
|
||||
// TODO: compose, replace {{ user }} tag, etc
|
||||
return {
|
||||
@@ -186,6 +259,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
function ensureSession(sessionId: string) {
|
||||
ensureSessionGeneration(sessionId)
|
||||
if (!sessionMessages.value[sessionId] || sessionMessages.value[sessionId].length === 0) {
|
||||
sessionMessages.value[sessionId] = [{
|
||||
...generateInitialMessage(),
|
||||
@@ -200,6 +274,11 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
ensureSession(activeSessionId.value)
|
||||
|
||||
function getSessionMessagesById(sessionId: string) {
|
||||
ensureSession(sessionId)
|
||||
return sessionMessages.value[sessionId]!
|
||||
}
|
||||
|
||||
const messages = computed<ChatEntry[]>({
|
||||
get: () => {
|
||||
ensureSession(activeSessionId.value)
|
||||
@@ -216,6 +295,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
function cleanupMessages(sessionId = activeSessionId.value) {
|
||||
bumpSessionGeneration(sessionId)
|
||||
sessionMessages.value[sessionId] = [{
|
||||
...generateInitialMessage(),
|
||||
context: {
|
||||
@@ -224,6 +304,17 @@ export const useChatStore = defineStore('chat', () => {
|
||||
ts: Date.now(),
|
||||
},
|
||||
}]
|
||||
// Reject pending sends for this session so callers don't hang after cleanup
|
||||
for (const queued of pendingQueuedSends.value) {
|
||||
if (queued.sessionId !== sessionId)
|
||||
continue
|
||||
|
||||
queued.cancelled = true
|
||||
queued.deferred.reject(new Error('Chat session was reset before send could start'))
|
||||
}
|
||||
pendingQueuedSends.value = pendingQueuedSends.value.filter(item => item.sessionId !== sessionId)
|
||||
sending.value = false
|
||||
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [] }
|
||||
}
|
||||
|
||||
function getAllSessions() {
|
||||
@@ -232,6 +323,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
function replaceSessions(sessions: Record<string, ChatEntry[]>) {
|
||||
sessionMessages.value = sessions
|
||||
sessionGenerations.value = Object.fromEntries(Object.keys(sessions).map(sessionId => [sessionId, 0]))
|
||||
const [firstSessionId] = Object.keys(sessions)
|
||||
if (!sessionMessages.value[activeSessionId.value] && firstSessionId)
|
||||
activeSessionId.value = firstSessionId
|
||||
@@ -241,6 +333,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
function resetAllSessions() {
|
||||
sessionMessages.value = {}
|
||||
sessionGenerations.value = {}
|
||||
activeSessionId.value = 'default'
|
||||
ensureSession(activeSessionId.value)
|
||||
}
|
||||
@@ -319,21 +412,28 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
|
||||
// ----- Send flow (user -> LLM -> assistant) -----
|
||||
const streamingMessage = ref<ChatAssistantMessage>({ role: 'assistant', content: '', slices: [], tool_results: [] })
|
||||
|
||||
async function send(
|
||||
async function performSend(
|
||||
sendingMessage: string,
|
||||
options: {
|
||||
model: string
|
||||
chatProvider: ChatProvider
|
||||
providerConfig?: Record<string, unknown>
|
||||
attachments?: { type: 'image', data: string, mimeType: string }[]
|
||||
tools?: StreamOptions['tools']
|
||||
},
|
||||
options: SendOptions,
|
||||
generation: number,
|
||||
sessionId: string,
|
||||
) {
|
||||
if (!sendingMessage && !options.attachments?.length)
|
||||
return
|
||||
|
||||
ensureSession(sessionId)
|
||||
|
||||
const isStaleGeneration = () => getSessionGeneration(sessionId) !== generation
|
||||
const shouldAbort = () => isStaleGeneration()
|
||||
if (shouldAbort())
|
||||
return
|
||||
sending.value = true
|
||||
const assistantContext: MessageContext = {
|
||||
sessionId,
|
||||
source: 'llm',
|
||||
ts: Date.now(),
|
||||
}
|
||||
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [], context: assistantContext }
|
||||
|
||||
try {
|
||||
await emitBeforeMessageComposedHooks(sendingMessage)
|
||||
@@ -355,8 +455,12 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
const finalContent = contentParts.length > 1 ? contentParts : sendingMessage
|
||||
|
||||
const userContext: MessageContext = { sessionId: activeSessionId.value, source: 'text', ts: Date.now() }
|
||||
messages.value.push({ role: 'user', content: finalContent, context: userContext })
|
||||
if (shouldAbort())
|
||||
return
|
||||
|
||||
const sessionMessagesForSend = getSessionMessagesById(sessionId)
|
||||
const userContext: MessageContext = { sessionId, source: 'text', ts: Date.now() }
|
||||
sessionMessagesForSend.push({ role: 'user', content: finalContent, context: userContext })
|
||||
|
||||
publishContextMessage({
|
||||
sessionId: userContext.sessionId,
|
||||
@@ -368,6 +472,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
const parser = useLlmmarkerParser({
|
||||
onLiteral: async (literal) => {
|
||||
if (shouldAbort())
|
||||
return
|
||||
await emitTokenLiteralHooks(literal)
|
||||
|
||||
streamingMessage.value.content += literal
|
||||
@@ -385,6 +491,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
})
|
||||
},
|
||||
onSpecial: async (special) => {
|
||||
if (shouldAbort())
|
||||
return
|
||||
await emitTokenSpecialHooks(special)
|
||||
},
|
||||
minLiteralEmitLength: 24, // Avoid emitting literals too fast. This is a magic number and can be changed later.
|
||||
@@ -393,6 +501,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const toolCallQueue = createQueue<ChatSlices>({
|
||||
handlers: [
|
||||
async (ctx) => {
|
||||
if (shouldAbort())
|
||||
return
|
||||
if (ctx.data.type === 'tool-call') {
|
||||
streamingMessage.value.slices.push(ctx.data)
|
||||
return
|
||||
@@ -405,9 +515,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
],
|
||||
})
|
||||
|
||||
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [] }
|
||||
|
||||
const newMessages = messages.value.map((msg) => {
|
||||
const newMessages = sessionMessagesForSend.map((msg) => {
|
||||
const { context: _context, ...withoutContext } = msg
|
||||
const rawMessage = toRaw(withoutContext)
|
||||
if (rawMessage.role === 'assistant') {
|
||||
@@ -427,6 +535,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
let fullText = ''
|
||||
const headers = (options.providerConfig?.headers || {}) as Record<string, string>
|
||||
|
||||
if (shouldAbort())
|
||||
return
|
||||
|
||||
await stream(options.model, options.chatProvider, newMessages as Message[], {
|
||||
headers,
|
||||
tools: options.tools,
|
||||
@@ -461,19 +572,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
await parser.end()
|
||||
|
||||
// Add the completed message to the history only if it has content
|
||||
if (streamingMessage.value.slices.length > 0) {
|
||||
const assistantContext: MessageContext = {
|
||||
sessionId: activeSessionId.value,
|
||||
source: 'llm',
|
||||
ts: Date.now(),
|
||||
}
|
||||
|
||||
if (!isStaleGeneration() && streamingMessage.value.slices.length > 0) {
|
||||
const assistantMessage: ChatEntry = {
|
||||
...(toRaw(streamingMessage.value) as ChatAssistantMessage),
|
||||
context: assistantContext,
|
||||
}
|
||||
|
||||
messages.value.push(assistantMessage)
|
||||
sessionMessagesForSend.push(assistantMessage)
|
||||
|
||||
publishContextMessage({
|
||||
sessionId: assistantContext.sessionId,
|
||||
@@ -515,6 +620,24 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function send(
|
||||
sendingMessage: string,
|
||||
options: SendOptions,
|
||||
) {
|
||||
const sessionId = activeSessionId.value
|
||||
const generation = getSessionGeneration(sessionId)
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
sendQueue.enqueue({
|
||||
sendingMessage,
|
||||
options,
|
||||
generation,
|
||||
sessionId,
|
||||
deferred: { resolve, reject },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
sending,
|
||||
activeSessionId,
|
||||
|
||||
Reference in New Issue
Block a user