diff --git a/app/client/api.ts b/app/client/api.ts
index cecc453baa2..671cb1c4840 100644
--- a/app/client/api.ts
+++ b/app/client/api.ts
@@ -59,7 +59,7 @@ export interface ChatOptions {
config: LLMConfig;
onUpdate?: (message: string, chunk: string) => void;
- onFinish: (message: string) => void;
+ onFinish: (message: string, finishedReason?: string) => void;
onError?: (err: Error) => void;
onController?: (controller: AbortController) => void;
onBeforeTool?: (tool: ChatMessageTool) => void;
diff --git a/app/client/controller.ts b/app/client/controller.ts
index a2e00173dd0..ac5bac7a1e4 100644
--- a/app/client/controller.ts
+++ b/app/client/controller.ts
@@ -26,6 +26,10 @@ export const ChatControllerPool = {
return Object.values(this.controllers).length > 0;
},
+ getPendingMessageId() {
+ return Object.keys(this.controllers).map((v) => v.split(",").at(-1));
+ },
+
remove(sessionId: string, messageId: string) {
const key = this.key(sessionId, messageId);
delete this.controllers[key];
diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts
index 7dd39c9cddc..eb3ab8f485b 100644
--- a/app/client/platforms/anthropic.ts
+++ b/app/client/platforms/anthropic.ts
@@ -262,7 +262,7 @@ export class ClaudeApi implements LLMApi {
runTools[index]["function"]["arguments"] +=
chunkJson?.delta?.partial_json;
}
- return chunkJson?.delta?.text;
+ return { delta: chunkJson?.delta?.text };
},
// processToolMessage, include tool_calls message and tool call results
(
diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts
index cd10d2f6c15..3b5590a9f9f 100644
--- a/app/client/platforms/moonshot.ts
+++ b/app/client/platforms/moonshot.ts
@@ -163,7 +163,7 @@ export class MoonshotApi implements LLMApi {
runTools[index]["function"]["arguments"] += args;
}
}
- return choices[0]?.delta?.content;
+ return { delta: choices[0]?.delta?.content };
},
// processToolMessage, include tool_calls message and tool call results
(
diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 664ff872ba3..aff9a2ca2d6 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -266,6 +266,7 @@ export class ChatGPTApi implements LLMApi {
content: string;
tool_calls: ChatMessageTool[];
};
+ finish_reason?: string;
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
@@ -286,7 +287,10 @@ export class ChatGPTApi implements LLMApi {
runTools[index]["function"]["arguments"] += args;
}
}
- return choices[0]?.delta?.content;
+ return {
+ delta: choices[0]?.delta?.content,
+ finishReason: choices[0]?.finish_reason,
+ };
},
// processToolMessage, include tool_calls message and tool call results
(
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 17f8d3a3496..b2b35c9ce27 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -9,6 +9,7 @@ import React, {
RefObject,
} from "react";
+import ContinueIcon from "../icons/continue.svg";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
import RenameIcon from "../icons/rename.svg";
@@ -461,7 +462,16 @@ export function ChatActions(props: {
// stop all responses
const couldStop = ChatControllerPool.hasPending();
- const stopAll = () => ChatControllerPool.stopAll();
+ const stopAll = () => {
+ const stopList = ChatControllerPool.getPendingMessageId();
+ ChatControllerPool.stopAll();
+ chatStore.updateCurrentSession(
+ (session) =>
+ (session.messages = session.messages.map((v) =>
+ stopList.includes(v.id) ? { ...v, finishedReason: "aborted" } : v,
+ )),
+ );
+ };
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
@@ -1045,6 +1055,12 @@ function _Chat() {
// stop response
const onUserStop = (messageId: string) => {
ChatControllerPool.stop(session.id, messageId);
+ chatStore.updateCurrentSession(
+ (session) =>
+ (session.messages = session.messages.map((v) =>
+ v.id === messageId ? { ...v, finishedReason: "aborted" } : v,
+ )),
+ );
};
useEffect(() => {
@@ -1171,6 +1187,18 @@ function _Chat() {
inputRef.current?.focus();
};
+ const onContinue = (messageID: string) => {
+ chatStore.updateCurrentSession(
+ (session) =>
+ (session.messages = session.messages.map((v) =>
+ v.id === messageID ? { ...v, streaming: true } : v,
+ )),
+ );
+ chatStore
+ .onContinueBotMessage(messageID)
+ .finally(() => setIsLoading(false));
+ };
+
const onPinMessage = (message: ChatMessage) => {
chatStore.updateCurrentSession((session) =>
session.mask.context.push(message),
@@ -1724,6 +1752,15 @@ function _Chat() {
)
}
/>
+ {["length", "aborted"].includes(
+ message.finishedReason ?? "",
+ ) ? (
+ }
+ onClick={() => onContinue(message.id)}
+ />
+ ) : null}
>
)}
diff --git a/app/icons/continue.svg b/app/icons/continue.svg
new file mode 100644
index 00000000000..d88f263f66e
--- /dev/null
+++ b/app/icons/continue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/locales/ar.ts b/app/locales/ar.ts
index 464519e2055..4a0885b1248 100644
--- a/app/locales/ar.ts
+++ b/app/locales/ar.ts
@@ -45,6 +45,7 @@ const ar: PartialLocaleType = {
Edit: "تحرير",
RefreshTitle: "تحديث العنوان",
RefreshToast: "تم إرسال طلب تحديث العنوان",
+ Continue: "استمر",
},
Commands: {
new: "دردشة جديدة",
diff --git a/app/locales/bn.ts b/app/locales/bn.ts
index 945c442df57..96fcdeefa44 100644
--- a/app/locales/bn.ts
+++ b/app/locales/bn.ts
@@ -45,6 +45,7 @@ const bn: PartialLocaleType = {
Edit: "সম্পাদনা করুন",
RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
+ Continue: "চালিয়ে যান",
},
Commands: {
new: "নতুন চ্যাট",
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index fcbdb6f627c..b606f158c21 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -46,6 +46,7 @@ const cn = {
FullScreen: "全屏",
RefreshTitle: "刷新标题",
RefreshToast: "已发送刷新标题请求",
+ Continue: "继续",
},
Commands: {
new: "新建聊天",
diff --git a/app/locales/cs.ts b/app/locales/cs.ts
index 5a132b3ce6d..ee243b9cbfd 100644
--- a/app/locales/cs.ts
+++ b/app/locales/cs.ts
@@ -45,6 +45,7 @@ const cs: PartialLocaleType = {
Edit: "Upravit",
RefreshTitle: "Obnovit název",
RefreshToast: "Požadavek na obnovení názvu byl odeslán",
+ Continue: "Pokračovat",
},
Commands: {
new: "Nová konverzace",
diff --git a/app/locales/de.ts b/app/locales/de.ts
index ebe7aff2d71..7f3bb73ef44 100644
--- a/app/locales/de.ts
+++ b/app/locales/de.ts
@@ -45,6 +45,7 @@ const de: PartialLocaleType = {
Edit: "Bearbeiten",
RefreshTitle: "Titel aktualisieren",
RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
+ Continue: "Fortsetzen",
},
Commands: {
new: "Neues Gespräch",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 049447569f7..456844a9006 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -47,6 +47,7 @@ const en: LocaleType = {
FullScreen: "FullScreen",
RefreshTitle: "Refresh Title",
RefreshToast: "Title refresh request sent",
+ Continue: "Continue",
},
Commands: {
new: "Start a new chat",
diff --git a/app/locales/es.ts b/app/locales/es.ts
index c1caae2d385..ef66a411b18 100644
--- a/app/locales/es.ts
+++ b/app/locales/es.ts
@@ -46,6 +46,7 @@ const es: PartialLocaleType = {
Edit: "Editar",
RefreshTitle: "Actualizar título",
RefreshToast: "Se ha enviado la solicitud de actualización del título",
+ Continue: "Continuar",
},
Commands: {
new: "Nueva conversación",
diff --git a/app/locales/fr.ts b/app/locales/fr.ts
index 97fb79e3238..f960eb6151f 100644
--- a/app/locales/fr.ts
+++ b/app/locales/fr.ts
@@ -45,6 +45,7 @@ const fr: PartialLocaleType = {
Edit: "Modifier",
RefreshTitle: "Actualiser le titre",
RefreshToast: "Demande d'actualisation du titre envoyée",
+ Continue: "Continuer",
},
Commands: {
new: "Nouvelle discussion",
diff --git a/app/locales/id.ts b/app/locales/id.ts
index 1eff667ed9e..64474f51cb1 100644
--- a/app/locales/id.ts
+++ b/app/locales/id.ts
@@ -45,6 +45,7 @@ const id: PartialLocaleType = {
Edit: "Edit",
RefreshTitle: "Segarkan Judul",
RefreshToast: "Permintaan penyegaran judul telah dikirim",
+ Continue: "Lanjutkan",
},
Commands: {
new: "Obrolan Baru",
diff --git a/app/locales/it.ts b/app/locales/it.ts
index 9b0d965d351..ceac7184b16 100644
--- a/app/locales/it.ts
+++ b/app/locales/it.ts
@@ -45,6 +45,7 @@ const it: PartialLocaleType = {
Edit: "Modifica",
RefreshTitle: "Aggiorna titolo",
RefreshToast: "Richiesta di aggiornamento del titolo inviata",
+ Continue: "Continua",
},
Commands: {
new: "Nuova chat",
diff --git a/app/locales/jp.ts b/app/locales/jp.ts
index 1511afc2c5f..82fbc4a9119 100644
--- a/app/locales/jp.ts
+++ b/app/locales/jp.ts
@@ -45,6 +45,7 @@ const jp: PartialLocaleType = {
Edit: "編集",
RefreshTitle: "タイトルを更新",
RefreshToast: "タイトル更新リクエストが送信されました",
+ Continue: "続ける",
},
Commands: {
new: "新しいチャット",
diff --git a/app/locales/ko.ts b/app/locales/ko.ts
index 11e6aa4eb29..56c2ab4c25c 100644
--- a/app/locales/ko.ts
+++ b/app/locales/ko.ts
@@ -45,6 +45,7 @@ const ko: PartialLocaleType = {
Edit: "편집",
RefreshTitle: "제목 새로고침",
RefreshToast: "제목 새로고침 요청이 전송되었습니다",
+ Continue: "계속하다",
},
Commands: {
new: "새 채팅",
diff --git a/app/locales/no.ts b/app/locales/no.ts
index 6fc8e86c787..291a47a5ab4 100644
--- a/app/locales/no.ts
+++ b/app/locales/no.ts
@@ -46,6 +46,7 @@ const no: PartialLocaleType = {
Edit: "Rediger",
RefreshTitle: "Oppdater tittel",
RefreshToast: "Forespørsel om titteloppdatering sendt",
+ Continue: "Fortsette",
},
Commands: {
new: "Ny samtale",
diff --git a/app/locales/pt.ts b/app/locales/pt.ts
index c04081a8b36..dad1ed56984 100644
--- a/app/locales/pt.ts
+++ b/app/locales/pt.ts
@@ -45,6 +45,7 @@ const pt: PartialLocaleType = {
Edit: "Editar",
RefreshTitle: "Atualizar Título",
RefreshToast: "Solicitação de atualização de título enviada",
+ Continue: "Continuar",
},
Commands: {
new: "Iniciar um novo chat",
diff --git a/app/locales/ru.ts b/app/locales/ru.ts
index 0fcf73a249f..1d32be226bc 100644
--- a/app/locales/ru.ts
+++ b/app/locales/ru.ts
@@ -45,6 +45,7 @@ const ru: PartialLocaleType = {
Edit: "Редактировать",
RefreshTitle: "Обновить заголовок",
RefreshToast: "Запрос на обновление заголовка отправлен",
+ Continue: "Продолжить",
},
Commands: {
new: "Новый чат",
diff --git a/app/locales/sk.ts b/app/locales/sk.ts
index 8f83c3ba73a..286b249e1c6 100644
--- a/app/locales/sk.ts
+++ b/app/locales/sk.ts
@@ -47,6 +47,7 @@ const sk: PartialLocaleType = {
Edit: "Upraviť",
RefreshTitle: "Obnoviť názov",
RefreshToast: "Požiadavka na obnovenie názvu bola odoslaná",
+ Continue: "Pokračovať",
},
Commands: {
new: "Začať nový chat",
diff --git a/app/locales/tr.ts b/app/locales/tr.ts
index b7f14104750..d89b15091e0 100644
--- a/app/locales/tr.ts
+++ b/app/locales/tr.ts
@@ -45,6 +45,7 @@ const tr: PartialLocaleType = {
Edit: "Düzenle",
RefreshTitle: "Başlığı Yenile",
RefreshToast: "Başlık yenileme isteği gönderildi",
+ Continue: "Devam et",
},
Commands: {
new: "Yeni sohbet",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index b0602a08174..c5bebaceee4 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -45,6 +45,7 @@ const tw = {
Edit: "編輯",
RefreshTitle: "刷新標題",
RefreshToast: "已發送刷新標題請求",
+ Continue: "繼續",
},
Commands: {
new: "新建聊天",
diff --git a/app/locales/vi.ts b/app/locales/vi.ts
index 1f20e15a0b1..5e9f2485ec3 100644
--- a/app/locales/vi.ts
+++ b/app/locales/vi.ts
@@ -45,6 +45,7 @@ const vi: PartialLocaleType = {
Edit: "Chỉnh sửa",
RefreshTitle: "Làm mới tiêu đề",
RefreshToast: "Đã gửi yêu cầu làm mới tiêu đề",
+ Continue: "Tiếp tục",
},
Commands: {
new: "Tạo cuộc trò chuyện mới",
diff --git a/app/store/chat.ts b/app/store/chat.ts
index 3bcda75389a..59c22f1493c 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -46,6 +46,7 @@ export type ChatMessage = RequestMessage & {
id: string;
model?: ModelType;
tools?: ChatMessageTool[];
+ finishedReason?: string;
};
export function createMessage(override: Partial): ChatMessage {
@@ -373,8 +374,10 @@ export const useChatStore = createPersistStore(
session.messages = session.messages.concat();
});
},
- onFinish(message) {
+ onFinish(message, finishedReason) {
botMessage.streaming = false;
+ if (finishedReason !== null && finishedReason !== undefined)
+ botMessage.finishedReason = finishedReason;
if (message) {
botMessage.content = message;
get().onNewMessage(botMessage);
@@ -429,6 +432,94 @@ export const useChatStore = createPersistStore(
});
},
+ async onContinueBotMessage(messageID: string) {
+ const session = get().currentSession();
+ const modelConfig = session.mask.modelConfig;
+
+ // get recent messages
+ const recentMessages = get().getMessagesWithMemory(messageID);
+ const messageIndex = get().currentSession().messages.length + 1;
+
+ const botMessage = session.messages.find((v) => v.id === messageID);
+
+ if (!botMessage) {
+ console.error("[Chat] failed to find bot message");
+ return;
+ }
+
+ const baseContent = botMessage.content;
+
+ const api: ClientApi = getClientApi(modelConfig.providerName);
+ // make request
+ api.llm.chat({
+ messages: recentMessages,
+ config: { ...modelConfig, stream: true },
+ onUpdate(message) {
+ botMessage.streaming = true;
+ if (message) {
+ botMessage.content = baseContent + message;
+ }
+ get().updateCurrentSession((session) => {
+ session.messages = session.messages.concat();
+ });
+ },
+ onFinish(message, finishedReason) {
+ botMessage.streaming = false;
+ if (finishedReason !== null && finishedReason !== undefined)
+ botMessage.finishedReason = finishedReason;
+ if (message) {
+ botMessage.content = baseContent + message;
+ get().onNewMessage(botMessage);
+ }
+ ChatControllerPool.remove(session.id, botMessage.id);
+ },
+ onBeforeTool(tool: ChatMessageTool) {
+ (botMessage.tools = botMessage?.tools || []).push(tool);
+ get().updateCurrentSession((session) => {
+ session.messages = session.messages.concat();
+ });
+ },
+ onAfterTool(tool: ChatMessageTool) {
+ botMessage?.tools?.forEach((t, i, tools) => {
+ if (tool.id == t.id) {
+ tools[i] = { ...tool };
+ }
+ });
+ get().updateCurrentSession((session) => {
+ session.messages = session.messages.concat();
+ });
+ },
+ onError(error) {
+ const isAborted = error.message?.includes?.("aborted");
+ botMessage.content +=
+ "\n\n" +
+ prettyObject({
+ error: true,
+ message: error.message,
+ });
+ botMessage.streaming = false;
+ botMessage.isError = !isAborted;
+ get().updateCurrentSession((session) => {
+ session.messages = session.messages.concat();
+ });
+ ChatControllerPool.remove(
+ session.id,
+ botMessage.id ?? messageIndex,
+ );
+
+ console.error("[Chat] failed ", error);
+ },
+ onController(controller) {
+ // collect controller for stop/retry
+ ChatControllerPool.addController(
+ session.id,
+ botMessage.id ?? messageIndex,
+ controller,
+ );
+ },
+ });
+ },
+
getMemoryPrompt() {
const session = get().currentSession();
@@ -441,12 +532,17 @@ export const useChatStore = createPersistStore(
}
},
- getMessagesWithMemory() {
+ getMessagesWithMemory(messageID?: string) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const clearContextIndex = session.clearContextIndex ?? 0;
const messages = session.messages.slice();
- const totalMessageCount = session.messages.length;
+ let messageIdx = session.messages.findIndex((v) => v.id === messageID);
+ if (messageIdx === -1) messageIdx = session.messages.length;
+ const totalMessageCount = Math.min(
+ messageIdx + 1,
+ session.messages.length,
+ );
// in-context prompts
const contextPrompts = session.mask.context.slice();
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index 7f3bb23c58e..8c04df4fc29 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -3,7 +3,7 @@ import {
UPLOAD_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
-import { RequestMessage } from "@/app/client/api";
+import { ChatOptions, RequestMessage } from "@/app/client/api";
import Locale from "@/app/locales";
import {
EventStreamContentType,
@@ -160,17 +160,21 @@ export function stream(
tools: any[],
funcs: Record,
controller: AbortController,
- parseSSE: (text: string, runTools: any[]) => string | undefined,
+ parseSSE: (
+ text: string,
+ runTools: any[],
+ ) => { delta?: string; finishReason?: string },
processToolMessage: (
requestPayload: any,
toolCallMessage: any,
toolCallResult: any[],
) => void,
- options: any,
+ options: ChatOptions,
) {
let responseText = "";
let remainText = "";
let finished = false;
+ let finishedReason: string | undefined;
let running = false;
let runTools: any[] = [];
@@ -254,14 +258,13 @@ export function stream(
chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
}, 60);
});
- return;
}
if (running) {
return;
}
console.debug("[ChatAPI] end");
finished = true;
- options.onFinish(responseText + remainText);
+ options.onFinish(responseText + remainText, finishedReason);
}
};
@@ -333,7 +336,11 @@ export function stream(
try {
const chunk = parseSSE(msg.data, runTools);
if (chunk) {
- remainText += chunk;
+ if (typeof chunk === "string") remainText += chunk;
+ else {
+ if (chunk.delta) remainText += chunk.delta;
+ finishedReason = chunk.finishReason;
+ }
}
} catch (e) {
console.error("[Request] parse error", text, msg, e);