Skip to content

Commit 85adc2f

Browse files
authored
Merge pull request #49 from beekeeper-studio/fix/empty-message-when-ai-is-interrupted
filter empty messages from conversation
2 parents 379d02b + 6fd1c63 commit 85adc2f

File tree

6 files changed

+69
-4
lines changed

6 files changed

+69
-4
lines changed

src/assets/styles/_theme.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
--theme-thinking-dot: var(--text);
1212
--theme-scrollbar-track: transparent;
1313
--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-base) 20%, var(--theme-bg) 80%);
14+
--text-muted: color-mix(in srgb, var(--theme-base) 20%, var(--theme-bg) 80%);
1415
}

src/assets/styles/components/_message.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@
3535
margin-top: 0.5rem;
3636
justify-content: flex-end;
3737
}
38+
39+
.message-content.literally-empty {
40+
font-style: italic;
41+
--theme-text-message-system: var(--text-muted);
42+
}
3843
}

src/components/ChatInterface.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
/>
2525
<div
2626
class="message error"
27-
v-if="error && !error.message.includes('User rejected tool call')"
27+
v-if="isUnexpectedError"
2828
>
2929
<div class="message-content">
3030
Something went wrong.
@@ -160,6 +160,26 @@ export default {
160160
(this.status === "submitted" || this.status === "streaming")
161161
);
162162
},
163+
isUnexpectedError() {
164+
if (!this.error) {
165+
return false;
166+
}
167+
168+
if (!this.error.message) {
169+
return true;
170+
}
171+
172+
if (this.error.message.includes('User rejected tool call')) {
173+
return false;
174+
}
175+
176+
// User aborted request before AI got a chance to respond
177+
if (this.error.message.includes('aborted without reason')) {
178+
return false;
179+
}
180+
181+
return true;
182+
},
163183
isErrorTruncated() {
164184
return this.error && this.error.toString().length > 300;
165185
},

src/components/messages/Message.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<template>
22
<div :class="['message', message.role]">
3-
<div class="message-content">
3+
<div class="message-content" :class="{ 'literally-empty': isEmpty }">
44
<template v-if="message.role === 'system'" />
55
<template v-else v-for="(part, index) of message.parts" :key="index">
66
<markdown v-if="part.type === 'text'" :content="part.text" />
77
<tool-message v-else-if="part.type === 'tool-invocation'" :toolCall="part.toolInvocation" :askingPermission="pendingToolCallIds.includes(part.toolInvocation.toolCallId)
88
" @accept="$emit('accept-permission', part.toolInvocation.toolCallId)"
99
@reject="$emit('reject-permission', part.toolInvocation.toolCallId)" />
1010
</template>
11+
<span v-if="isEmpty">
12+
Empty response
13+
</span>
1114
</div>
1215
<div class="message-actions" v-if="status ==='ready'">
1316
<button class="btn btn-flat-2 copy-btn" :class="{ copied }" @click="handleCopyClick">
@@ -28,6 +31,7 @@ import { UIMessage } from "ai";
2831
import Markdown from "@/components/messages/Markdown.vue";
2932
import ToolMessage from "@/components/messages/ToolMessage.vue";
3033
import { clipboard } from "@beekeeperstudio/plugin";
34+
import { isEmptyUIMessage } from "@/utils";
3135
3236
export default {
3337
name: "Message",
@@ -75,6 +79,9 @@ export default {
7579
}
7680
return text.trim();
7781
},
82+
isEmpty() {
83+
return isEmptyUIMessage(this.message);
84+
},
7885
},
7986
8087
methods: {

src/composables/ai.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useChat } from "@ai-sdk/vue";
2-
import { computed, ref, watch } from "vue";
2+
import { computed, nextTick, ref, watch } from "vue";
33
import {
44
AvailableProviders,
55
AvailableModels,
@@ -11,7 +11,7 @@ import { notify } from "@beekeeperstudio/plugin";
1111
import { z } from "zod";
1212
import { createProvider } from "@/providers";
1313
import { useConfigurationStore } from "@/stores/configuration";
14-
import { isReadQuery } from "@/utils";
14+
import { isEmptyUIMessage, isReadQuery } from "@/utils";
1515

1616
type AIOptions = {
1717
initialMessages: Message[];
@@ -27,6 +27,9 @@ type SendOptions = {
2727
}
2828

2929
export function useAI(options: AIOptions) {
30+
/** FIXME: Only used because we want to retry automatically after an error.
31+
* REMOVE AFTER V5 UPGRADE. */
32+
const sendOptions = ref<SendOptions>();
3033
const pendingToolCallIds = ref<string[]>([]);
3134
const askingPermission = computed(() => pendingToolCallIds.value.length > 0);
3235
const followupAfterRejected = ref("");
@@ -90,6 +93,13 @@ export function useAI(options: AIOptions) {
9093
followupAfterRejected.value = "";
9194
// fillTitle();
9295
}
96+
} else if (error.message.includes("all messages must have non-empty content")) {
97+
// FIXME we dont need this once we upgrade to AI SDK v5 since we use `convertToModelMessages()`
98+
// See https://ai-sdk.dev/docs/troubleshooting/use-chat-tools-no-response
99+
messages.value = messages.value.filter((m) => !isEmptyUIMessage(m));
100+
nextTick().then(() => {
101+
retry(sendOptions.value!);
102+
})
93103
}
94104
},
95105
onFinish: () => {
@@ -150,6 +160,8 @@ export function useAI(options: AIOptions) {
150160

151161
/** Send a message to the AI */
152162
async function send(message: string, options: SendOptions) {
163+
// FIXME: Remove after v5 upgrade
164+
sendOptions.value = options;
153165
await append(
154166
{
155167
role: "user",
@@ -165,6 +177,8 @@ export function useAI(options: AIOptions) {
165177
}
166178

167179
async function retry(options: SendOptions) {
180+
// FIXME: Remove after v5 upgrade
181+
sendOptions.value = options;
168182
await reload({
169183
body: {
170184
sendOptions: options,

src/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { UIMessage } from "ai";
12
import { Model } from "./stores/chat";
3+
import _ from "lodash";
24
import { identify } from "sql-query-identifier";
35

46
export function safeJSONStringify(value: any, ...args: any): string {
@@ -85,3 +87,19 @@ export function isReadQuery(query: string) {
8587
return false;
8688
}
8789
}
90+
91+
export function isEmptyUIMessage(message: UIMessage): boolean {
92+
const nonEmptyParts = message.parts.filter((part) => {
93+
if (part.type === "step-start") {
94+
return false;
95+
}
96+
97+
if (part.type === "text" && _.isEmpty(part.text)) {
98+
return false;
99+
}
100+
101+
return true;
102+
});
103+
104+
return nonEmptyParts.length === 0;
105+
}

0 commit comments

Comments
 (0)