forked from XiaoMo/ChatGPT-Next-Web
merge
This commit is contained in:
commit
a8a8becf96
@ -12,7 +12,7 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: rgb(51, 51, 51);
|
color: var(--black);
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@ -59,6 +59,7 @@ export function ChatList() {
|
|||||||
state.removeSession,
|
state.removeSession,
|
||||||
state.moveSession,
|
state.moveSession,
|
||||||
]);
|
]);
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = (result) => {
|
const onDragEnd: OnDragEndResponder = (result) => {
|
||||||
const { destination, source } = result;
|
const { destination, source } = result;
|
||||||
@ -95,10 +96,7 @@ export function ChatList() {
|
|||||||
index={i}
|
index={i}
|
||||||
selected={i === selectedIndex}
|
selected={i === selectedIndex}
|
||||||
onClick={() => selectSession(i)}
|
onClick={() => selectSession(i)}
|
||||||
onDelete={() =>
|
onDelete={chatStore.deleteSession}
|
||||||
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
|
|
||||||
removeSession(i)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
@ -4,7 +4,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
|||||||
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 ExportIcon from "../icons/export.svg";
|
import ExportIcon from "../icons/export.svg";
|
||||||
import MenuIcon from "../icons/menu.svg";
|
import ReturnIcon from "../icons/return.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
import DownloadIcon from "../icons/download.svg";
|
import DownloadIcon from "../icons/download.svg";
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
@ -404,6 +404,7 @@ export function Chat(props: {
|
|||||||
|
|
||||||
// submit user input
|
// submit user input
|
||||||
const onUserSubmit = () => {
|
const onUserSubmit = () => {
|
||||||
|
if (userInput.length <= 0) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
@ -420,7 +421,6 @@ export function Chat(props: {
|
|||||||
// check if should send message
|
// check if should send message
|
||||||
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (shouldSubmit(e)) {
|
if (shouldSubmit(e)) {
|
||||||
setAutoScroll(true);
|
|
||||||
onUserSubmit();
|
onUserSubmit();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@ -507,13 +507,10 @@ export function Chat(props: {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.chat} key={session.id}>
|
<div className={styles.chat} key={session.id}>
|
||||||
<div className={styles["window-header"]}>
|
<div className={styles["window-header"]}>
|
||||||
<div
|
<div className={styles["window-header-title"]}>
|
||||||
className={styles["window-header-title"]}
|
|
||||||
onClick={props?.showSideBar}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
onClick={() => {
|
onClickCapture={() => {
|
||||||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||||
if (newTopic && newTopic !== session.topic) {
|
if (newTopic && newTopic !== session.topic) {
|
||||||
chatStore.updateCurrentSession(
|
chatStore.updateCurrentSession(
|
||||||
@ -531,7 +528,7 @@ export function Chat(props: {
|
|||||||
<div className={styles["window-actions"]}>
|
<div className={styles["window-actions"]}>
|
||||||
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<MenuIcon />}
|
icon={<ReturnIcon />}
|
||||||
bordered
|
bordered
|
||||||
title={Locale.Chat.Actions.ChatList}
|
title={Locale.Chat.Actions.ChatList}
|
||||||
onClick={props?.showSideBar}
|
onClick={props?.showSideBar}
|
||||||
@ -667,7 +664,7 @@ export function Chat(props: {
|
|||||||
onInput={(e) => onInput(e.currentTarget.value)}
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onKeyDown={onInputKeyDown}
|
onKeyDown={onInputKeyDown}
|
||||||
onFocus={() => setAutoScroll(isMobileScreen())}
|
onFocus={() => setAutoScroll(true)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
setTimeout(() => setPromptHints([]), 500);
|
setTimeout(() => setPromptHints([]), 500);
|
||||||
@ -679,7 +676,6 @@ export function Chat(props: {
|
|||||||
text={Locale.Chat.Send}
|
text={Locale.Chat.Send}
|
||||||
className={styles["chat-input-send"]}
|
className={styles["chat-input-send"]}
|
||||||
noDark
|
noDark
|
||||||
disabled={!userInput}
|
|
||||||
onClick={onUserSubmit}
|
onClick={onUserSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,6 +95,7 @@ function _Home() {
|
|||||||
state.removeSession,
|
state.removeSession,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
const chatStore = useChatStore();
|
||||||
const loading = !useHasHydrated();
|
const loading = !useHasHydrated();
|
||||||
const [showSideBar, setShowSideBar] = useState(true);
|
const [showSideBar, setShowSideBar] = useState(true);
|
||||||
|
|
||||||
@ -144,11 +145,7 @@ function _Home() {
|
|||||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon />}
|
icon={<CloseIcon />}
|
||||||
onClick={() => {
|
onClick={chatStore.deleteSession}
|
||||||
if (confirm(Locale.Home.DeleteChat)) {
|
|
||||||
removeSession(currentIndex);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["sidebar-action"]}>
|
<div className={styles["sidebar-action"]}>
|
||||||
|
@ -135,9 +135,25 @@
|
|||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
padding: 10px 30px;
|
padding: 10px 20px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
margin-bottom: 20px;
|
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;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,17 +110,37 @@ export function showModal(props: ModalProps) {
|
|||||||
root.render(<Modal {...props} onClose={closeModal}></Modal>);
|
root.render(<Modal {...props} onClose={closeModal}></Modal>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToastProps = { content: string };
|
export type ToastProps = {
|
||||||
|
content: string;
|
||||||
|
action?: {
|
||||||
|
text: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function Toast(props: ToastProps) {
|
export function Toast(props: ToastProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles["toast-container"]}>
|
<div className={styles["toast-container"]}>
|
||||||
<div className={styles["toast-content"]}>{props.content}</div>
|
<div className={styles["toast-content"]}>
|
||||||
|
<span>{props.content}</span>
|
||||||
|
{props.action && (
|
||||||
|
<button
|
||||||
|
onClick={props.action.onClick}
|
||||||
|
className={styles["toast-action"]}
|
||||||
|
>
|
||||||
|
{props.action.text}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showToast(content: string, delay = 3000) {
|
export function showToast(
|
||||||
|
content: string,
|
||||||
|
action?: ToastProps["action"],
|
||||||
|
delay = 3000,
|
||||||
|
) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = styles.show;
|
div.className = styles.show;
|
||||||
document.body.appendChild(div);
|
document.body.appendChild(div);
|
||||||
@ -139,7 +159,7 @@ export function showToast(content: string, delay = 3000) {
|
|||||||
close();
|
close();
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
||||||
root.render(<Toast content={content} />);
|
root.render(<Toast content={content} action={action} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
|
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
|
||||||
|
21
app/icons/return.svg
Normal file
21
app/icons/return.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<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(2 2.6666666666666665) rotate(0 1.1666333333333334 2.1666666666666665)"
|
||||||
|
d="M2.33,0L0,2L2.33,4.33 " />
|
||||||
|
<path id="路径 2"
|
||||||
|
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||||
|
transform="translate(2 4.666666666666666) rotate(0 6.000006859869576 4.333333333333333)"
|
||||||
|
d="M0,0L7.66,0C9.96,0 11.91,1.87 12,4.17C12.09,6.59 10.09,8.67 7.66,8.67L2,8.67 " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1013 B |
@ -47,6 +47,8 @@ const cn = {
|
|||||||
Home: {
|
Home: {
|
||||||
NewChat: "新的聊天",
|
NewChat: "新的聊天",
|
||||||
DeleteChat: "确认删除选中的对话?",
|
DeleteChat: "确认删除选中的对话?",
|
||||||
|
DeleteToast: "已删除会话",
|
||||||
|
Revert: "撤销",
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
Title: "设置",
|
Title: "设置",
|
||||||
|
@ -50,6 +50,8 @@ const en: LocaleType = {
|
|||||||
Home: {
|
Home: {
|
||||||
NewChat: "New Chat",
|
NewChat: "New Chat",
|
||||||
DeleteChat: "Confirm to delete the selected conversation?",
|
DeleteChat: "Confirm to delete the selected conversation?",
|
||||||
|
DeleteToast: "Chat Deleted",
|
||||||
|
Revert: "Revert",
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
Title: "Settings",
|
Title: "Settings",
|
||||||
|
@ -50,6 +50,8 @@ const es: LocaleType = {
|
|||||||
Home: {
|
Home: {
|
||||||
NewChat: "Nuevo chat",
|
NewChat: "Nuevo chat",
|
||||||
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
|
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
|
||||||
|
DeleteToast: "Chat Deleted",
|
||||||
|
Revert: "Revert",
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
Title: "Configuración",
|
Title: "Configuración",
|
||||||
|
@ -50,6 +50,8 @@ const it: LocaleType = {
|
|||||||
Home: {
|
Home: {
|
||||||
NewChat: "Nuova Chat",
|
NewChat: "Nuova Chat",
|
||||||
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
||||||
|
DeleteToast: "Chat Deleted",
|
||||||
|
Revert: "Revert",
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
Title: "Impostazioni",
|
Title: "Impostazioni",
|
||||||
|
@ -48,6 +48,8 @@ const tw: LocaleType = {
|
|||||||
Home: {
|
Home: {
|
||||||
NewChat: "新的對話",
|
NewChat: "新的對話",
|
||||||
DeleteChat: "確定要刪除選取的對話嗎?",
|
DeleteChat: "確定要刪除選取的對話嗎?",
|
||||||
|
DeleteToast: "已刪除對話",
|
||||||
|
Revert: "撤銷",
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
Title: "設定",
|
Title: "設定",
|
||||||
|
@ -7,9 +7,10 @@ import {
|
|||||||
requestChatStream,
|
requestChatStream,
|
||||||
requestWithPrompt,
|
requestWithPrompt,
|
||||||
} from "../requests";
|
} from "../requests";
|
||||||
import { trimTopic } from "../utils";
|
import { isMobileScreen, trimTopic } from "../utils";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
import { showToast } from "../components/ui-lib";
|
||||||
|
|
||||||
export type Message = ChatCompletionResponseMessage & {
|
export type Message = ChatCompletionResponseMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
@ -204,6 +205,7 @@ interface ChatStore {
|
|||||||
moveSession: (from: number, to: number) => void;
|
moveSession: (from: number, to: number) => void;
|
||||||
selectSession: (index: number) => void;
|
selectSession: (index: number) => void;
|
||||||
newSession: () => void;
|
newSession: () => void;
|
||||||
|
deleteSession: () => void;
|
||||||
currentSession: () => ChatSession;
|
currentSession: () => ChatSession;
|
||||||
onNewMessage: (message: Message) => void;
|
onNewMessage: (message: Message) => void;
|
||||||
onUserInput: (content: string) => Promise<void>;
|
onUserInput: (content: string) => Promise<void>;
|
||||||
@ -324,6 +326,26 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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() {
|
currentSession() {
|
||||||
let index = get().currentSessionIndex;
|
let index = get().currentSessionIndex;
|
||||||
const sessions = get().sessions;
|
const sessions = get().sessions;
|
||||||
|
16
app/utils.ts
16
app/utils.ts
@ -7,11 +7,9 @@ export function trimTopic(topic: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function copyToClipboard(text: string) {
|
export async function copyToClipboard(text: string) {
|
||||||
if (navigator.clipboard) {
|
try {
|
||||||
navigator.clipboard.writeText(text).catch((err) => {
|
await navigator.clipboard.writeText(text);
|
||||||
console.error("Failed to copy: ", err);
|
} catch (error) {
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const textArea = document.createElement("textarea");
|
const textArea = document.createElement("textarea");
|
||||||
textArea.value = text;
|
textArea.value = text;
|
||||||
document.body.appendChild(textArea);
|
document.body.appendChild(textArea);
|
||||||
@ -19,11 +17,11 @@ export async function copyToClipboard(text: string) {
|
|||||||
textArea.select();
|
textArea.select();
|
||||||
try {
|
try {
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
console.log("Text copied to clipboard");
|
} catch (error) {
|
||||||
} catch (err) {
|
showToast(Locale.Copy.Failed);
|
||||||
console.error("Failed to copy: ", err);
|
|
||||||
}
|
}
|
||||||
document.body.removeChild(textArea);
|
} finally {
|
||||||
|
showToast(Locale.Copy.Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,3 +126,13 @@ OpenAI只接受指定地区的信用卡(中国信用卡无法使用)。一
|
|||||||
|
|
||||||
## 如何使用 Azure OpenAI 接口
|
## 如何使用 Azure OpenAI 接口
|
||||||
请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
|
请参考:[#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 爆破记录,那么就是密码被爆破了。
|
||||||
|
Loading…
Reference in New Issue
Block a user