mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-25 18:26:48 +08:00
Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web
This commit is contained in:
@@ -19,6 +19,7 @@ import LightIcon from "../icons/light.svg";
|
||||
import DarkIcon from "../icons/dark.svg";
|
||||
import AutoIcon from "../icons/auto.svg";
|
||||
import BottomIcon from "../icons/bottom.svg";
|
||||
import StopIcon from "../icons/pause.svg";
|
||||
|
||||
import {
|
||||
Message,
|
||||
@@ -38,7 +39,6 @@ import {
|
||||
isMobileScreen,
|
||||
selectOrCopy,
|
||||
autoGrowTextArea,
|
||||
getCSSVar,
|
||||
} from "../utils";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -355,8 +355,8 @@ export function ChatActions(props: {
|
||||
}) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// switch themes
|
||||
const theme = chatStore.config.theme;
|
||||
|
||||
function nextTheme() {
|
||||
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
||||
const themeIndex = themes.indexOf(theme);
|
||||
@@ -365,8 +365,20 @@ export function ChatActions(props: {
|
||||
chatStore.updateConfig((config) => (config.theme = nextTheme));
|
||||
}
|
||||
|
||||
// stop all responses
|
||||
const couldStop = ControllerPool.hasPending();
|
||||
const stopAll = () => ControllerPool.stopAll();
|
||||
|
||||
return (
|
||||
<div className={chatStyle["chat-input-actions"]}>
|
||||
{couldStop && (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
onClick={stopAll}
|
||||
>
|
||||
<StopIcon />
|
||||
</div>
|
||||
)}
|
||||
{!props.hitBottom && (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
@@ -524,21 +536,45 @@ export function Chat(props: {
|
||||
}
|
||||
};
|
||||
|
||||
const onResend = (botIndex: number) => {
|
||||
const findLastUesrIndex = (messageId: number) => {
|
||||
// find last user input message and resend
|
||||
for (let i = botIndex; i >= 0; i -= 1) {
|
||||
if (messages[i].role === "user") {
|
||||
setIsLoading(true);
|
||||
chatStore
|
||||
.onUserInput(messages[i].content)
|
||||
.then(() => setIsLoading(false));
|
||||
chatStore.updateCurrentSession((session) =>
|
||||
session.messages.splice(i, 2),
|
||||
);
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
let lastUserMessageIndex: number | null = null;
|
||||
for (let i = 0; i < session.messages.length; i += 1) {
|
||||
const message = session.messages[i];
|
||||
if (message.id === messageId) {
|
||||
break;
|
||||
}
|
||||
if (message.role === "user") {
|
||||
lastUserMessageIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return lastUserMessageIndex;
|
||||
};
|
||||
|
||||
const deleteMessage = (userIndex: number) => {
|
||||
chatStore.updateCurrentSession((session) =>
|
||||
session.messages.splice(userIndex, 2),
|
||||
);
|
||||
};
|
||||
|
||||
const onDelete = (botMessageId: number) => {
|
||||
const userIndex = findLastUesrIndex(botMessageId);
|
||||
if (userIndex === null) return;
|
||||
deleteMessage(userIndex);
|
||||
};
|
||||
|
||||
const onResend = (botMessageId: number) => {
|
||||
// find last user input message and resend
|
||||
const userIndex = findLastUesrIndex(botMessageId);
|
||||
if (userIndex === null) return;
|
||||
|
||||
setIsLoading(true);
|
||||
chatStore
|
||||
.onUserInput(session.messages[userIndex].content)
|
||||
.then(() => setIsLoading(false));
|
||||
deleteMessage(userIndex);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const config = useChatStore((state) => state.config);
|
||||
@@ -710,12 +746,20 @@ export function Chat(props: {
|
||||
{Locale.Chat.Actions.Stop}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Retry}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Delete}
|
||||
</div>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Retry}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8.002766666666666 2) rotate(0 0 4.649916666666667)" d="M0,9.3L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 7.333333333333333) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 14) rotate(0 4 0)" d="M8,0L0,0 " /></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 4) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 8) rotate(0 4 2)" d="M8,0L4,4L0,0 " /></g></g></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 958 B After Width: | Height: | Size: 736 B |
1
app/icons/pause.svg
Normal file
1
app/icons/pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M13.33,6.67C13.33,2.98 10.35,0 6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 6) rotate(0 0 2)" d="M0,0L0,4 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666666666666666 6) rotate(0 0 2)" d="M0,0L0,4 " /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -17,6 +17,7 @@ const cn = {
|
||||
Copy: "复制",
|
||||
Stop: "停止",
|
||||
Retry: "重试",
|
||||
Delete: "删除",
|
||||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
|
||||
@@ -19,6 +19,7 @@ const de: LocaleType = {
|
||||
Copy: "Kopieren",
|
||||
Stop: "Stop",
|
||||
Retry: "Wiederholen",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Rename: "Chat umbenennen",
|
||||
Typing: "Tippen...",
|
||||
|
||||
@@ -19,6 +19,7 @@ const en: LocaleType = {
|
||||
Copy: "Copy",
|
||||
Stop: "Stop",
|
||||
Retry: "Retry",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Rename: "Rename Chat",
|
||||
Typing: "Typing…",
|
||||
|
||||
@@ -19,6 +19,7 @@ const es: LocaleType = {
|
||||
Copy: "Copiar",
|
||||
Stop: "Detener",
|
||||
Retry: "Reintentar",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Rename: "Renombrar chat",
|
||||
Typing: "Escribiendo...",
|
||||
|
||||
@@ -54,23 +54,13 @@ export function getLang(): Lang {
|
||||
|
||||
const lang = getLanguage();
|
||||
|
||||
if (lang.includes("zh") || lang.includes("cn")) {
|
||||
return "cn";
|
||||
} else if (lang.includes("tw")) {
|
||||
return "tw";
|
||||
} else if (lang.includes("es")) {
|
||||
return "es";
|
||||
} else if (lang.includes("it")) {
|
||||
return "it";
|
||||
} else if (lang.includes("tr")) {
|
||||
return "tr";
|
||||
} else if (lang.includes("jp")) {
|
||||
return "jp";
|
||||
} else if (lang.includes("de")) {
|
||||
return "de";
|
||||
} else {
|
||||
return "en";
|
||||
for (const option of AllLangs) {
|
||||
if (lang.includes(option)) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return "en";
|
||||
}
|
||||
|
||||
export function changeLang(lang: Lang) {
|
||||
@@ -87,4 +77,4 @@ export default {
|
||||
tr: TR,
|
||||
jp: JP,
|
||||
de: DE,
|
||||
}[getLang()];
|
||||
}[getLang()] as typeof CN;
|
||||
|
||||
@@ -19,6 +19,7 @@ const it: LocaleType = {
|
||||
Copy: "Copia",
|
||||
Stop: "Stop",
|
||||
Retry: "Riprova",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Rename: "Rinomina Chat",
|
||||
Typing: "Typing…",
|
||||
|
||||
@@ -18,6 +18,7 @@ const jp = {
|
||||
Copy: "コピー",
|
||||
Stop: "停止",
|
||||
Retry: "リトライ",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Rename: "チャットの名前を変更",
|
||||
Typing: "入力中…",
|
||||
@@ -178,6 +179,4 @@ const jp = {
|
||||
},
|
||||
};
|
||||
|
||||
export type LocaleType = typeof jp;
|
||||
|
||||
export default jp;
|
||||
|
||||
@@ -19,6 +19,7 @@ const tr: LocaleType = {
|
||||
Copy: "Kopyala",
|
||||
Stop: "Durdur",
|
||||
Retry: "Tekrar Dene",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Rename: "Sohbeti Yeniden Adlandır",
|
||||
Typing: "Yazıyor…",
|
||||
|
||||
@@ -18,6 +18,7 @@ const tw: LocaleType = {
|
||||
Copy: "複製",
|
||||
Stop: "停止",
|
||||
Retry: "重試",
|
||||
Delete: "刪除",
|
||||
},
|
||||
Rename: "重命名對話",
|
||||
Typing: "正在輸入…",
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ChatRequest, ChatResponse } from "./api/openai/typing";
|
||||
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
|
||||
const TIME_OUT_MS = 30000;
|
||||
const TIME_OUT_MS = 60000;
|
||||
|
||||
const makeRequestParam = (
|
||||
messages: Message[],
|
||||
@@ -167,15 +167,14 @@ export async function requestChatStream(
|
||||
options?.onController?.(controller);
|
||||
|
||||
while (true) {
|
||||
// handle time out, will stop if no response in 10 secs
|
||||
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
|
||||
const content = await reader?.read();
|
||||
clearTimeout(resTimeoutId);
|
||||
|
||||
|
||||
if (!content || !content.value) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const text = decoder.decode(content.value, { stream: true });
|
||||
responseText += text;
|
||||
|
||||
@@ -235,6 +234,14 @@ export const ControllerPool = {
|
||||
controller?.abort();
|
||||
},
|
||||
|
||||
stopAll() {
|
||||
Object.values(this.controllers).forEach((v) => v.abort());
|
||||
},
|
||||
|
||||
hasPending() {
|
||||
return Object.values(this.controllers).length > 0;
|
||||
},
|
||||
|
||||
remove(sessionIndex: number, messageId: number) {
|
||||
const key = this.key(sessionIndex, messageId);
|
||||
delete this.controllers[key];
|
||||
|
||||
@@ -386,6 +386,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
const botMessage: Message = createMessage({
|
||||
role: "assistant",
|
||||
streaming: true,
|
||||
id: userMessage.id! + 1,
|
||||
});
|
||||
|
||||
// get recent messages
|
||||
@@ -421,7 +422,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
onError(error, statusCode) {
|
||||
if (statusCode === 401) {
|
||||
botMessage.content = Locale.Error.Unauthorized;
|
||||
} else {
|
||||
} else if (!error.message.includes("aborted")) {
|
||||
botMessage.content += "\n\n" + Locale.Store.Error;
|
||||
}
|
||||
botMessage.streaming = false;
|
||||
|
||||
@@ -116,10 +116,9 @@ export const usePromptStore = create<PromptStore>()(
|
||||
})
|
||||
.concat([...(state?.prompts?.values() ?? [])]);
|
||||
|
||||
const allPromptsForSearch = builtinPrompts.reduce(
|
||||
(pre, cur) => pre.concat(cur),
|
||||
[],
|
||||
);
|
||||
const allPromptsForSearch = builtinPrompts
|
||||
.reduce((pre, cur) => pre.concat(cur), [])
|
||||
.filter((v) => !!v.title && !!v.content);
|
||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||
SearchService.init(allPromptsForSearch);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user