}
+ icon={
}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar}
@@ -668,7 +664,7 @@ export function Chat(props: {
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}
- onFocus={() => setAutoScroll(isMobileScreen())}
+ onFocus={() => setAutoScroll(true)}
onBlur={() => {
setAutoScroll(false);
setTimeout(() => setPromptHints([]), 500);
diff --git a/app/components/home.tsx b/app/components/home.tsx
index 4d1203df0..ac66e322c 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -93,6 +93,7 @@ function _Home() {
state.removeSession,
],
);
+ const chatStore = useChatStore();
const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true);
@@ -143,11 +144,7 @@ function _Home() {
}
- onClick={() => {
- if (confirm(Locale.Home.DeleteChat)) {
- removeSession(currentIndex);
- }
- }}
+ onClick={chatStore.deleteSession}
/>
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss
index 95091cd0a..83eb614f7 100644
--- a/app/components/ui-lib.module.scss
+++ b/app/components/ui-lib.module.scss
@@ -135,9 +135,25 @@
box-shadow: var(--card-shadow);
border: var(--border-in-light);
color: var(--black);
- padding: 10px 30px;
+ padding: 10px 20px;
border-radius: 50px;
margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+
+ .toast-action {
+ padding-left: 20px;
+ color: var(--primary);
+ opacity: 0.8;
+ border: 0;
+ background: none;
+ cursor: pointer;
+ font-family: inherit;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
}
}
@@ -160,4 +176,4 @@
max-height: 50vh;
}
}
-}
\ No newline at end of file
+}
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index 6761e7f97..a72aa868f 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -110,17 +110,37 @@ export function showModal(props: ModalProps) {
root.render(
);
}
-export type ToastProps = { content: string };
+export type ToastProps = {
+ content: string;
+ action?: {
+ text: string;
+ onClick: () => void;
+ };
+};
export function Toast(props: ToastProps) {
return (
-
{props.content}
+
+ {props.content}
+ {props.action && (
+
+ )}
+
);
}
-export function showToast(content: string, delay = 3000) {
+export function showToast(
+ content: string,
+ action?: ToastProps["action"],
+ delay = 3000,
+) {
const div = document.createElement("div");
div.className = styles.show;
document.body.appendChild(div);
@@ -139,7 +159,7 @@ export function showToast(content: string, delay = 3000) {
close();
}, delay);
- root.render(
);
+ root.render(
);
}
export type InputProps = React.HTMLProps
& {
diff --git a/app/icons/return.svg b/app/icons/return.svg
new file mode 100644
index 000000000..eba5e78f9
--- /dev/null
+++ b/app/icons/return.svg
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 143f4d636..02913eb23 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -47,6 +47,8 @@ const cn = {
Home: {
NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?",
+ DeleteToast: "已删除会话",
+ Revert: "撤销",
},
Settings: {
Title: "设置",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 1960192e3..4779f95dc 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -50,6 +50,8 @@ const en: LocaleType = {
Home: {
NewChat: "New Chat",
DeleteChat: "Confirm to delete the selected conversation?",
+ DeleteToast: "Chat Deleted",
+ Revert: "Revert",
},
Settings: {
Title: "Settings",
diff --git a/app/locales/es.ts b/app/locales/es.ts
index 11bd5db51..1b3ebf11d 100644
--- a/app/locales/es.ts
+++ b/app/locales/es.ts
@@ -50,6 +50,8 @@ const es: LocaleType = {
Home: {
NewChat: "Nuevo chat",
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
+ DeleteToast: "Chat Deleted",
+ Revert: "Revert",
},
Settings: {
Title: "Configuración",
diff --git a/app/locales/it.ts b/app/locales/it.ts
index 56f5d27d3..bf1a320c7 100644
--- a/app/locales/it.ts
+++ b/app/locales/it.ts
@@ -50,6 +50,8 @@ const it: LocaleType = {
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
+ DeleteToast: "Chat Deleted",
+ Revert: "Revert",
},
Settings: {
Title: "Impostazioni",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index 63b75e111..353a74d9b 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -48,6 +48,8 @@ const tw: LocaleType = {
Home: {
NewChat: "新的對話",
DeleteChat: "確定要刪除選取的對話嗎?",
+ DeleteToast: "已刪除對話",
+ Revert: "撤銷",
},
Settings: {
Title: "設定",
diff --git a/app/store/app.ts b/app/store/app.ts
index 2f90c17c6..8c33c3ab8 100644
--- a/app/store/app.ts
+++ b/app/store/app.ts
@@ -7,9 +7,10 @@ import {
requestChatStream,
requestWithPrompt,
} from "../requests";
-import { trimTopic } from "../utils";
+import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales";
+import { showToast } from "../components/ui-lib";
export type Message = ChatCompletionResponseMessage & {
date: string;
@@ -206,6 +207,7 @@ interface ChatStore {
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
+ deleteSession: () => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise;
@@ -326,6 +328,26 @@ export const useChatStore = create()(
}));
},
+ deleteSession() {
+ const deletedSession = get().currentSession();
+ const index = get().currentSessionIndex;
+ const isLastSession = get().sessions.length === 1;
+ if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
+ get().removeSession(index);
+ }
+ showToast(Locale.Home.DeleteToast, {
+ text: Locale.Home.Revert,
+ onClick() {
+ set((state) => ({
+ sessions: state.sessions
+ .slice(0, index)
+ .concat([deletedSession])
+ .concat(state.sessions.slice(index + Number(isLastSession))),
+ }));
+ },
+ });
+ },
+
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
diff --git a/app/utils.ts b/app/utils.ts
index 9fcb11820..bb44e072d 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -7,23 +7,21 @@ export function trimTopic(topic: string) {
}
export async function copyToClipboard(text: string) {
- if (navigator.clipboard) {
- navigator.clipboard.writeText(text).catch(err => {
- console.error('Failed to copy: ', err);
- });
- } else {
- const textArea = document.createElement('textarea');
+ try {
+ await navigator.clipboard.writeText(text);
+ } catch (error) {
+ const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
- document.execCommand('copy');
- console.log('Text copied to clipboard');
- } catch (err) {
- console.error('Failed to copy: ', err);
+ document.execCommand("copy");
+ } catch (error) {
+ showToast(Locale.Copy.Failed);
}
- document.body.removeChild(textArea);
+ } finally {
+ showToast(Locale.Copy.Success);
}
}
diff --git a/docs/faq-cn.md b/docs/faq-cn.md
index b26bdedb1..88293b9c6 100644
--- a/docs/faq-cn.md
+++ b/docs/faq-cn.md
@@ -126,3 +126,13 @@ OpenAI只接受指定地区的信用卡(中国信用卡无法使用)。一
## 如何使用 Azure OpenAI 接口
请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
+
+## 为什么我的 Token 消耗得这么快?
+> 相关讨论:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
+- 如果你有 GPT 4 的权限,并且日常在使用 GPT 4 api,那么由于 GPT 4 价格是 GPT 3.5 的 15 倍左右,你的账单金额会急速膨胀;
+- 如果你在使用 GPT 3.5,并且使用频率并不高,仍然发现自己的账单金额在飞快增加,那么请马上按照以下步骤排查:
+ - 去 openai 官网查看你的 api key 消费记录,如果你的 token 每小时都有消费,并且每次都消耗了上万 token,那你的 key 一定是泄露了,请立即删除重新生成。**不要在乱七八糟的网站上查余额。**
+ - 如果你的密码设置很短,比如 5 位以内的字母,那么爆破成本是非常低的,建议你搜索一下 docker 的日志记录,确认是否有人大量尝试了密码组合,关键字:got access code
+- 通过上述两个方法就可以定位到你的 token 被快速消耗的原因:
+ - 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露;
+ - 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。