This commit is contained in:
skymkmk 2024-09-18 11:54:10 +08:00 committed by GitHub
commit 69bfc8ba4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 182 additions and 14 deletions

View File

@ -58,7 +58,7 @@ export interface ChatOptions {
config: LLMConfig; config: LLMConfig;
onUpdate?: (message: string, chunk: string) => void; onUpdate?: (message: string, chunk: string) => void;
onFinish: (message: string) => void; onFinish: (message: string, finishedReason?: string) => void;
onError?: (err: Error) => void; onError?: (err: Error) => void;
onController?: (controller: AbortController) => void; onController?: (controller: AbortController) => void;
onBeforeTool?: (tool: ChatMessageTool) => void; onBeforeTool?: (tool: ChatMessageTool) => void;

View File

@ -26,6 +26,10 @@ export const ChatControllerPool = {
return Object.values(this.controllers).length > 0; return Object.values(this.controllers).length > 0;
}, },
getPendingMessageId() {
return Object.keys(this.controllers).map((v) => v.split(",").at(-1));
},
remove(sessionId: string, messageId: string) { remove(sessionId: string, messageId: string) {
const key = this.key(sessionId, messageId); const key = this.key(sessionId, messageId);
delete this.controllers[key]; delete this.controllers[key];

View File

@ -255,7 +255,7 @@ export class ClaudeApi implements LLMApi {
runTools[index]["function"]["arguments"] += runTools[index]["function"]["arguments"] +=
chunkJson?.delta?.partial_json; chunkJson?.delta?.partial_json;
} }
return chunkJson?.delta?.text; return { delta: chunkJson?.delta?.text };
}, },
// processToolMessage, include tool_calls message and tool call results // processToolMessage, include tool_calls message and tool call results
( (

View File

@ -144,7 +144,7 @@ export class MoonshotApi implements LLMApi {
runTools[index]["function"]["arguments"] += args; 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 // processToolMessage, include tool_calls message and tool call results
( (

View File

@ -260,6 +260,7 @@ export class ChatGPTApi implements LLMApi {
content: string; content: string;
tool_calls: ChatMessageTool[]; tool_calls: ChatMessageTool[];
}; };
finish_reason?: string;
}>; }>;
const tool_calls = choices[0]?.delta?.tool_calls; const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) { if (tool_calls?.length > 0) {
@ -280,7 +281,10 @@ export class ChatGPTApi implements LLMApi {
runTools[index]["function"]["arguments"] += args; 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 // processToolMessage, include tool_calls message and tool call results
( (

View File

@ -9,6 +9,7 @@ import React, {
RefObject, RefObject,
} from "react"; } from "react";
import ContinueIcon from "../icons/continue.svg";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
import RenameIcon from "../icons/rename.svg"; import RenameIcon from "../icons/rename.svg";
@ -460,7 +461,16 @@ export function ChatActions(props: {
// stop all responses // stop all responses
const couldStop = ChatControllerPool.hasPending(); 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 // switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model; const currentModel = chatStore.currentSession().mask.modelConfig.model;
@ -1044,6 +1054,12 @@ function _Chat() {
// stop response // stop response
const onUserStop = (messageId: string) => { const onUserStop = (messageId: string) => {
ChatControllerPool.stop(session.id, messageId); ChatControllerPool.stop(session.id, messageId);
chatStore.updateCurrentSession(
(session) =>
(session.messages = session.messages.map((v) =>
v.id === messageId ? { ...v, finishedReason: "aborted" } : v,
)),
);
}; };
useEffect(() => { useEffect(() => {
@ -1170,6 +1186,18 @@ function _Chat() {
inputRef.current?.focus(); 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) => { const onPinMessage = (message: ChatMessage) => {
chatStore.updateCurrentSession((session) => chatStore.updateCurrentSession((session) =>
session.mask.context.push(message), session.mask.context.push(message),
@ -1723,6 +1751,15 @@ function _Chat() {
) )
} }
/> />
{["length", "aborted"].includes(
message.finishedReason ?? "",
) ? (
<ChatAction
text={Locale.Chat.Actions.Continue}
icon={<ContinueIcon />}
onClick={() => onContinue(message.id)}
/>
) : null}
</> </>
)} )}
</div> </div>

1
app/icons/continue.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1726395286651" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10075" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M427.84 911.648a79.616 79.616 0 0 1-79.68-79.712V191.328a79.68 79.68 0 0 1 122.24-67.36l506.784 320.448a79.296 79.296 0 0 1 37.056 67.328c0 27.488-13.888 52.672-37.12 67.328L470.368 899.328a79.424 79.424 0 0 1-42.528 12.32z m16.32-690.688v581.376l459.808-290.624L444.16 220.96zM65.728 911.648a48 48 0 0 1-48-48v-704a48 48 0 1 1 96 0v704a48 48 0 0 1-48 48z" fill="#040000" p-id="10076"></path></svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@ -45,6 +45,7 @@ const ar: PartialLocaleType = {
Edit: "تحرير", Edit: "تحرير",
RefreshTitle: "تحديث العنوان", RefreshTitle: "تحديث العنوان",
RefreshToast: "تم إرسال طلب تحديث العنوان", RefreshToast: "تم إرسال طلب تحديث العنوان",
Continue: "استمر",
}, },
Commands: { Commands: {
new: "دردشة جديدة", new: "دردشة جديدة",

View File

@ -45,6 +45,7 @@ const bn: PartialLocaleType = {
Edit: "সম্পাদনা করুন", Edit: "সম্পাদনা করুন",
RefreshTitle: "শিরোনাম রিফ্রেশ করুন", RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে", RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
Continue: "চালিয়ে যান",
}, },
Commands: { Commands: {
new: "নতুন চ্যাট", new: "নতুন চ্যাট",

View File

@ -45,6 +45,7 @@ const cn = {
FullScreen: "全屏", FullScreen: "全屏",
RefreshTitle: "刷新标题", RefreshTitle: "刷新标题",
RefreshToast: "已发送刷新标题请求", RefreshToast: "已发送刷新标题请求",
Continue: "继续",
}, },
Commands: { Commands: {
new: "新建聊天", new: "新建聊天",

View File

@ -45,6 +45,7 @@ const cs: PartialLocaleType = {
Edit: "Upravit", Edit: "Upravit",
RefreshTitle: "Obnovit název", RefreshTitle: "Obnovit název",
RefreshToast: "Požadavek na obnovení názvu byl odeslán", RefreshToast: "Požadavek na obnovení názvu byl odeslán",
Continue: "Pokračovat",
}, },
Commands: { Commands: {
new: "Nová konverzace", new: "Nová konverzace",

View File

@ -45,6 +45,7 @@ const de: PartialLocaleType = {
Edit: "Bearbeiten", Edit: "Bearbeiten",
RefreshTitle: "Titel aktualisieren", RefreshTitle: "Titel aktualisieren",
RefreshToast: "Anfrage zur Titelaktualisierung gesendet", RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
Continue: "Fortsetzen",
}, },
Commands: { Commands: {
new: "Neues Gespräch", new: "Neues Gespräch",

View File

@ -47,6 +47,7 @@ const en: LocaleType = {
FullScreen: "FullScreen", FullScreen: "FullScreen",
RefreshTitle: "Refresh Title", RefreshTitle: "Refresh Title",
RefreshToast: "Title refresh request sent", RefreshToast: "Title refresh request sent",
Continue: "Continue",
}, },
Commands: { Commands: {
new: "Start a new chat", new: "Start a new chat",

View File

@ -46,6 +46,7 @@ const es: PartialLocaleType = {
Edit: "Editar", Edit: "Editar",
RefreshTitle: "Actualizar título", RefreshTitle: "Actualizar título",
RefreshToast: "Se ha enviado la solicitud de actualización del título", RefreshToast: "Se ha enviado la solicitud de actualización del título",
Continue: "Continuar",
}, },
Commands: { Commands: {
new: "Nueva conversación", new: "Nueva conversación",

View File

@ -45,6 +45,7 @@ const fr: PartialLocaleType = {
Edit: "Modifier", Edit: "Modifier",
RefreshTitle: "Actualiser le titre", RefreshTitle: "Actualiser le titre",
RefreshToast: "Demande d'actualisation du titre envoyée", RefreshToast: "Demande d'actualisation du titre envoyée",
Continue: "Continuer",
}, },
Commands: { Commands: {
new: "Nouvelle discussion", new: "Nouvelle discussion",

View File

@ -45,6 +45,7 @@ const id: PartialLocaleType = {
Edit: "Edit", Edit: "Edit",
RefreshTitle: "Segarkan Judul", RefreshTitle: "Segarkan Judul",
RefreshToast: "Permintaan penyegaran judul telah dikirim", RefreshToast: "Permintaan penyegaran judul telah dikirim",
Continue: "Lanjutkan",
}, },
Commands: { Commands: {
new: "Obrolan Baru", new: "Obrolan Baru",

View File

@ -45,6 +45,7 @@ const it: PartialLocaleType = {
Edit: "Modifica", Edit: "Modifica",
RefreshTitle: "Aggiorna titolo", RefreshTitle: "Aggiorna titolo",
RefreshToast: "Richiesta di aggiornamento del titolo inviata", RefreshToast: "Richiesta di aggiornamento del titolo inviata",
Continue: "Continua",
}, },
Commands: { Commands: {
new: "Nuova chat", new: "Nuova chat",

View File

@ -45,6 +45,7 @@ const jp: PartialLocaleType = {
Edit: "編集", Edit: "編集",
RefreshTitle: "タイトルを更新", RefreshTitle: "タイトルを更新",
RefreshToast: "タイトル更新リクエストが送信されました", RefreshToast: "タイトル更新リクエストが送信されました",
Continue: "続ける",
}, },
Commands: { Commands: {
new: "新しいチャット", new: "新しいチャット",

View File

@ -45,6 +45,7 @@ const ko: PartialLocaleType = {
Edit: "편집", Edit: "편집",
RefreshTitle: "제목 새로고침", RefreshTitle: "제목 새로고침",
RefreshToast: "제목 새로고침 요청이 전송되었습니다", RefreshToast: "제목 새로고침 요청이 전송되었습니다",
Continue: "계속하다",
}, },
Commands: { Commands: {
new: "새 채팅", new: "새 채팅",

View File

@ -46,6 +46,7 @@ const no: PartialLocaleType = {
Edit: "Rediger", Edit: "Rediger",
RefreshTitle: "Oppdater tittel", RefreshTitle: "Oppdater tittel",
RefreshToast: "Forespørsel om titteloppdatering sendt", RefreshToast: "Forespørsel om titteloppdatering sendt",
Continue: "Fortsette",
}, },
Commands: { Commands: {
new: "Ny samtale", new: "Ny samtale",

View File

@ -45,6 +45,7 @@ const pt: PartialLocaleType = {
Edit: "Editar", Edit: "Editar",
RefreshTitle: "Atualizar Título", RefreshTitle: "Atualizar Título",
RefreshToast: "Solicitação de atualização de título enviada", RefreshToast: "Solicitação de atualização de título enviada",
Continue: "Continuar",
}, },
Commands: { Commands: {
new: "Iniciar um novo chat", new: "Iniciar um novo chat",

View File

@ -45,6 +45,7 @@ const ru: PartialLocaleType = {
Edit: "Редактировать", Edit: "Редактировать",
RefreshTitle: "Обновить заголовок", RefreshTitle: "Обновить заголовок",
RefreshToast: "Запрос на обновление заголовка отправлен", RefreshToast: "Запрос на обновление заголовка отправлен",
Continue: "Продолжить",
}, },
Commands: { Commands: {
new: "Новый чат", new: "Новый чат",

View File

@ -46,6 +46,7 @@ const sk: PartialLocaleType = {
Edit: "Upraviť", Edit: "Upraviť",
RefreshTitle: "Obnoviť názov", RefreshTitle: "Obnoviť názov",
RefreshToast: "Požiadavka na obnovenie názvu bola odoslaná", RefreshToast: "Požiadavka na obnovenie názvu bola odoslaná",
Continue: "Pokračovať",
}, },
Commands: { Commands: {
new: "Začať nový chat", new: "Začať nový chat",

View File

@ -45,6 +45,7 @@ const tr: PartialLocaleType = {
Edit: "Düzenle", Edit: "Düzenle",
RefreshTitle: "Başlığı Yenile", RefreshTitle: "Başlığı Yenile",
RefreshToast: "Başlık yenileme isteği gönderildi", RefreshToast: "Başlık yenileme isteği gönderildi",
Continue: "Devam et",
}, },
Commands: { Commands: {
new: "Yeni sohbet", new: "Yeni sohbet",

View File

@ -45,6 +45,7 @@ const tw = {
Edit: "編輯", Edit: "編輯",
RefreshTitle: "刷新標題", RefreshTitle: "刷新標題",
RefreshToast: "已發送刷新標題請求", RefreshToast: "已發送刷新標題請求",
Continue: "繼續",
}, },
Commands: { Commands: {
new: "新建聊天", new: "新建聊天",

View File

@ -45,6 +45,7 @@ const vi: PartialLocaleType = {
Edit: "Chỉnh sửa", Edit: "Chỉnh sửa",
RefreshTitle: "Làm mới tiêu đề", RefreshTitle: "Làm mới tiêu đề",
RefreshToast: "Đã gửi yêu cầu 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: { Commands: {
new: "Tạo cuộc trò chuyện mới", new: "Tạo cuộc trò chuyện mới",

View File

@ -46,6 +46,7 @@ export type ChatMessage = RequestMessage & {
id: string; id: string;
model?: ModelType; model?: ModelType;
tools?: ChatMessageTool[]; tools?: ChatMessageTool[];
finishedReason?: string;
}; };
export function createMessage(override: Partial<ChatMessage>): ChatMessage { export function createMessage(override: Partial<ChatMessage>): ChatMessage {
@ -373,8 +374,10 @@ export const useChatStore = createPersistStore(
session.messages = session.messages.concat(); session.messages = session.messages.concat();
}); });
}, },
onFinish(message) { onFinish(message, finishedReason) {
botMessage.streaming = false; botMessage.streaming = false;
if (finishedReason !== null && finishedReason !== undefined)
botMessage.finishedReason = finishedReason;
if (message) { if (message) {
botMessage.content = message; botMessage.content = message;
get().onNewMessage(botMessage); 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() { getMemoryPrompt() {
const session = get().currentSession(); const session = get().currentSession();
@ -441,12 +532,17 @@ export const useChatStore = createPersistStore(
} }
}, },
getMessagesWithMemory() { getMessagesWithMemory(messageID?: string) {
const session = get().currentSession(); const session = get().currentSession();
const modelConfig = session.mask.modelConfig; const modelConfig = session.mask.modelConfig;
const clearContextIndex = session.clearContextIndex ?? 0; const clearContextIndex = session.clearContextIndex ?? 0;
const messages = session.messages.slice(); 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 // in-context prompts
const contextPrompts = session.mask.context.slice(); const contextPrompts = session.mask.context.slice();

View File

@ -3,7 +3,7 @@ import {
UPLOAD_URL, UPLOAD_URL,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
} from "@/app/constant"; } from "@/app/constant";
import { RequestMessage } from "@/app/client/api"; import { ChatOptions, RequestMessage } from "@/app/client/api";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { import {
EventStreamContentType, EventStreamContentType,
@ -160,17 +160,21 @@ export function stream(
tools: any[], tools: any[],
funcs: Record<string, Function>, funcs: Record<string, Function>,
controller: AbortController, controller: AbortController,
parseSSE: (text: string, runTools: any[]) => string | undefined, parseSSE: (
text: string,
runTools: any[],
) => { delta?: string; finishReason?: string },
processToolMessage: ( processToolMessage: (
requestPayload: any, requestPayload: any,
toolCallMessage: any, toolCallMessage: any,
toolCallResult: any[], toolCallResult: any[],
) => void, ) => void,
options: any, options: ChatOptions,
) { ) {
let responseText = ""; let responseText = "";
let remainText = ""; let remainText = "";
let finished = false; let finished = false;
let finishedReason: string | undefined;
let running = false; let running = false;
let runTools: any[] = []; let runTools: any[] = [];
@ -254,14 +258,13 @@ export function stream(
chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
}, 60); }, 60);
}); });
return;
} }
if (running) { if (running) {
return; return;
} }
console.debug("[ChatAPI] end"); console.debug("[ChatAPI] end");
finished = true; finished = true;
options.onFinish(responseText + remainText); options.onFinish(responseText + remainText, finishedReason);
} }
}; };
@ -333,7 +336,11 @@ export function stream(
try { try {
const chunk = parseSSE(msg.data, runTools); const chunk = parseSSE(msg.data, runTools);
if (chunk) { if (chunk) {
remainText += chunk; if (typeof chunk === "string") remainText += chunk;
else {
if (chunk.delta) remainText += chunk.delta;
finishedReason = chunk.finishReason;
}
} }
} catch (e) { } catch (e) {
console.error("[Request] parse error", text, msg, e); console.error("[Request] parse error", text, msg, e);