Skip to content

Commit 3eb3649

Browse files
committed
Supports multiple concurrent and interruptible chats
1 parent 93dbf16 commit 3eb3649

File tree

7 files changed

+74
-32
lines changed

7 files changed

+74
-32
lines changed

src/renderer/components/common/SamplingCard.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ function handleError(errorMessage: string | null) {
4040
jsonError.value = errorMessage
4141
}
4242
43+
const samplingId = ref('')
44+
4345
const tryCompletions = () => {
4446
if (jsonError.value) {
4547
snackbarStore.showErrorMessage(jsonError.value)
4648
} else {
4749
const { messages, ...restParams } = samplingParams.value as SamplingRequest
4850
restParams.target = samplingResults.value
49-
createCompletion(messages, historyStore.getDate(), restParams)
51+
samplingId.value = historyStore.getDate()
52+
createCompletion(messages, samplingId.value, restParams)
5053
}
5154
}
5255
@@ -162,7 +165,7 @@ function continueAutoSampling() {
162165
unwatch()
163166
}, 30000) // 30 sec
164167
const unwatch = watch(
165-
() => messageStore.generating,
168+
() => samplingId.value in messageStore.generating,
166169
(val) => {
167170
if (val) {
168171
// Generation has started, the timer is no longer needed. The watch will track the progress.
@@ -242,7 +245,7 @@ function continueAutoSampling() {
242245
v-model="samplingProgress.percent"
243246
class="ml-8 mr-4"
244247
color="primary"
245-
:indeterminate="Boolean(messageStore.generating)"
248+
:indeterminate="samplingId in messageStore.generating"
246249
rounded
247250
@click="samplingProgress.auto = false"
248251
></v-progress-linear>

src/renderer/components/pages/ChatInputPage.vue

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,35 @@ const agentStore = useAgentStore()
6868
<!-- Adds a horizontal flex container (row direction) to override parent column layout, ensuring right alignment within v-col -->
6969
<div class="d-flex justify-end">
7070
<v-spacer></v-spacer>
71+
72+
<div v-if="messageStore.historyId in messageStore.generating">
73+
<v-icon-btn
74+
v-tooltip:start="$t('chat.wipe')"
75+
color="error"
76+
variant="tonal"
77+
icon="mdi-delete-outline"
78+
rounded="lg"
79+
@click="messageStore.init()"
80+
></v-icon-btn>
81+
<v-divider class="mx-1" vertical></v-divider>
82+
<v-icon-btn
83+
color="primary"
84+
variant="tonal"
85+
icon="mdi-stop"
86+
rounded="lg"
87+
@click="messageStore.stop"
88+
></v-icon-btn>
89+
</div>
90+
7191
<v-icon-btn
72-
v-if="messageStore.userMessage"
92+
v-else-if="messageStore.userMessage"
7393
color="primary"
7494
variant="tonal"
7595
icon="mdi-arrow-up"
7696
rounded="lg"
7797
@click="messageStore.sendMessage"
7898
></v-icon-btn>
79-
<v-icon-btn
80-
v-else-if="messageStore.generating"
81-
color="primary"
82-
variant="tonal"
83-
icon="mdi-stop"
84-
rounded="lg"
85-
@click="messageStore.stop"
86-
></v-icon-btn>
99+
87100
<div v-else-if="messageStore.conversation.length > 0">
88101
<v-icon-btn
89102
v-tooltip:start="$t('chat.wipe')"

src/renderer/composables/chatCompletions.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ export const createCompletion = async (
9595
}, [] as RequestMessageType[])
9696
// const conversation = rawconversation
9797
try {
98-
console.log('Chat session started: ', sessionId)
99-
messageStore.generating = sessionId
10098
// Create a completion (axios is not used here because it does not support streaming)
10199

102100
const headers: HeadersInit = {
@@ -173,9 +171,17 @@ export const createCompletion = async (
173171
body: JSON.stringify(body)
174172
}
175173

174+
const abortController = new AbortController()
175+
176+
console.log('Chat session started: ', sessionId)
177+
messageStore.generating[sessionId] = abortController
178+
176179
const completion = await fetch(
177180
chatbotStore.url + (chatbotStore.path ? chatbotStore.path : ''),
178-
request
181+
{
182+
...request,
183+
signal: abortController.signal
184+
}
179185
)
180186

181187
console.log(completion)
@@ -227,8 +233,9 @@ export const createCompletion = async (
227233
} catch (error: any) {
228234
snackbarStore.showErrorMessage(error?.message)
229235
} finally {
230-
if (messageStore.generating === sessionId) {
231-
messageStore.generating = ''
236+
if (sessionId in messageStore.generating) {
237+
messageStore.generating[sessionId].abort()
238+
delete messageStore.generating[sessionId]
232239
}
233240
}
234241
}
@@ -244,16 +251,17 @@ const read = async (
244251
const decoder = new TextDecoder()
245252
const messageStore = useMessageStore()
246253

247-
if (!messageStore.generating || messageStore.generating !== sessionId) {
254+
if (!(sessionId in messageStore.generating)) {
248255
return reader.releaseLock()
249256
}
250257
// Destructure the value returned by reader.read()
251258
const { done, value } = await reader.read()
252259

253260
// If the stream is done reading, release the lock on the reader
254261
if (done) {
255-
if (messageStore.generating === sessionId) {
256-
messageStore.generating = ''
262+
if (sessionId in messageStore.generating) {
263+
messageStore.generating[sessionId].abort()
264+
delete messageStore.generating[sessionId]
257265
}
258266
return reader.releaseLock()
259267
}

src/renderer/screens/PopupScreen.vue

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ const commandNotify = (item: PopupPromptsType) => {
2727
<template>
2828
<v-app>
2929
<v-layout>
30-
<v-app-bar
31-
class="py-2 px-3 drag gradient-command"
32-
density="compact"
33-
>
30+
<v-app-bar class="py-2 px-3 drag gradient-command" density="compact">
3431
<v-text-field
3532
v-model="search"
3633
class="no-drag"

src/renderer/screens/chat/ChatHistoryScreen.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script setup lang="tsx">
22
import { useHistoryStore } from '@/renderer/store/history'
3+
import { useMessageStore } from '@/renderer/store/message'
34
const historyStore = useHistoryStore()
5+
const messageStore = useMessageStore()
46
57
function parseContent(content) {
68
if (typeof content === 'string') {
@@ -30,6 +32,18 @@ function parseContent(content) {
3032
:subtitle="parseContent(item.messages.at(-1)?.content)"
3133
@click="historyStore.select(index)"
3234
>
35+
<template #append>
36+
<v-list-item-action>
37+
<v-icon-btn
38+
:loading="item.id in messageStore.generating"
39+
icon="mdi-delete-outline"
40+
rounded="lg"
41+
size="small"
42+
@click="historyStore.deleteById(index)"
43+
>
44+
</v-icon-btn>
45+
</v-list-item-action>
46+
</template>
3347
</v-list-item>
3448
</v-list>
3549
</template>

src/renderer/store/history.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const useHistoryStore = defineStore('historyStore', {
5757
},
5858
select(index: number) {
5959
const messageStore = useMessageStore()
60+
messageStore.historyId = this.conversation[index].id
6061
messageStore.conversation = this.conversation[index].messages
6162
},
6263
getColor(index: number) {

src/renderer/store/message.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface MessageStoreState {
1515
conversation: ChatConversationMessage[]
1616
historyId: string
1717
base64: string
18-
generating: string
18+
generating: Record<string, AbortController>
1919
}
2020

2121
export const useMessageStore = defineStore('messageStore', {
@@ -24,7 +24,7 @@ export const useMessageStore = defineStore('messageStore', {
2424
conversation: [],
2525
historyId: '',
2626
base64: '',
27-
generating: ''
27+
generating: {}
2828
}),
2929
actions: {
3030
init() {
@@ -41,8 +41,10 @@ export const useMessageStore = defineStore('messageStore', {
4141
this.historyId = ''
4242
},
4343
stop() {
44+
const id = this.historyId
4445
const snackbarStore = useSnackbarStore()
45-
this.generating = ''
46+
this.generating[id].abort()
47+
delete this.generating[id]
4648
snackbarStore.showInfoMessage('snackbar.stopped')
4749
},
4850
clear() {
@@ -70,7 +72,7 @@ export const useMessageStore = defineStore('messageStore', {
7072
// when role == "user" is found,drop followings
7173
if (index >= 0) {
7274
this.conversation.splice(index + 1)
73-
this.startInference()
75+
return this.startInference()
7476
}
7577
},
7678
sendMessage() {
@@ -89,7 +91,7 @@ export const useMessageStore = defineStore('messageStore', {
8991
role: 'user'
9092
})
9193

92-
this.startInference()
94+
return this.startInference()
9395
}
9496
},
9597
syncHistory: function (): string {
@@ -114,11 +116,15 @@ export const useMessageStore = defineStore('messageStore', {
114116
this.initConversation(messages)
115117
// this.syncHistory()
116118
},
117-
startInference: async function () {
119+
startInference: function () {
118120
const historyId = this.syncHistory()
119121
this.clear()
120-
await createCompletion(this.conversation, historyId)
121-
await this.postToolCall()
122+
123+
createCompletion(this.conversation, historyId).then(() => {
124+
this.postToolCall()
125+
})
126+
127+
return historyId
122128
},
123129
postToolCall: async function () {
124130
const mcpStore = useMcpStore()

0 commit comments

Comments
 (0)