Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
bb45c62a81 | ||
|
1e89fe14ac | ||
|
86507fa569 | ||
|
4180363f58 | ||
|
a5ec15236a | ||
|
43b6835564 | ||
|
3f865ffa1e |
26
README.md
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
One-Click to deploy your own ChatGPT web UI.
|
One-Click to deploy your own ChatGPT web UI.
|
||||||
|
|
||||||
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈问题 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈问题 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
||||||
|
|
||||||
@@ -17,15 +17,6 @@ One-Click to deploy your own ChatGPT web UI.
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 重要说明 Attention
|
|
||||||
本项目的演示地址所用的 OpenAI 账户的免费额度将于 2023-04-01 过期,届时将无法通过演示地址在线体验。
|
|
||||||
|
|
||||||
如果你想贡献出自己的 API Key,可以通过作者主页的邮箱发送给作者,并标注过期时间,在此提前感谢!
|
|
||||||
|
|
||||||
The free trial of the OpenAI account used by the demo will expire on April 1, 2023, and the demo will not be available at that time.
|
|
||||||
|
|
||||||
If you would like to contribute your API key, you can email it to the author and indicate the expiration date of the API key. Thank you in advance!
|
|
||||||
|
|
||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||||
@@ -93,7 +84,7 @@ You can star or watch this project or follow author to get release notifictions
|
|||||||
code1,code2,code3
|
code1,code2,code3
|
||||||
```
|
```
|
||||||
|
|
||||||
增加或修改该环境变量后,请重新部署项目使改动生效。
|
增加或修改该环境变量后,请**重新部署**项目使改动生效。
|
||||||
|
|
||||||
This project provides limited access control. Please add an environment variable named `CODE` on the environment variables page. The value should be a custom control code separated by comma like this:
|
This project provides limited access control. Please add an environment variable named `CODE` on the environment variables page. The value should be a custom control code separated by comma like this:
|
||||||
|
|
||||||
@@ -151,6 +142,19 @@ how to deploy nextjs project with docker on my ubuntu server, the build command
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 说明 Attention
|
||||||
|
本项目的演示地址所用的 OpenAI 账户的免费额度将于 2023-04-01 过期,届时将无法通过演示地址在线体验。
|
||||||
|
|
||||||
|
如果你想贡献出自己的 API Key,可以通过作者主页的邮箱发送给作者,并标注过期时间。
|
||||||
|
|
||||||
|
The free trial of the OpenAI account used by the demo will expire on April 1, 2023, and the demo will not be available at that time.
|
||||||
|
|
||||||
|
If you would like to contribute your API key, you can email it to the author and indicate the expiration date of the API key.
|
||||||
|
|
||||||
|
## 鸣谢 Special Thanks
|
||||||
|
### 捐赠者 Sponsor
|
||||||
|
[@mushan0x0](https://github.com/mushan0x0)
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
|
||||||
|
@@ -27,6 +27,7 @@ import Locale from "../locales";
|
|||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { REPO_URL } from "../constant";
|
import { REPO_URL } from "../constant";
|
||||||
|
import { ControllerPool } from "../requests";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@@ -146,28 +147,67 @@ function useSubmitHandler() {
|
|||||||
export function Chat(props: { showSideBar?: () => void }) {
|
export function Chat(props: { showSideBar?: () => void }) {
|
||||||
type RenderMessage = Message & { preview?: boolean };
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
const session = useChatStore((state) => state.currentSession());
|
const [session, sessionIndex] = useChatStore((state) => [
|
||||||
|
state.currentSession(),
|
||||||
|
state.currentSessionIndex,
|
||||||
|
]);
|
||||||
const [userInput, setUserInput] = useState("");
|
const [userInput, setUserInput] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||||
|
|
||||||
const onUserInput = useChatStore((state) => state.onUserInput);
|
const onUserInput = useChatStore((state) => state.onUserInput);
|
||||||
|
|
||||||
|
// submit user input
|
||||||
const onUserSubmit = () => {
|
const onUserSubmit = () => {
|
||||||
if (userInput.length <= 0) return;
|
if (userInput.length <= 0) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
onUserInput(userInput).then(() => setIsLoading(false));
|
onUserInput(userInput).then(() => setIsLoading(false));
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// stop response
|
||||||
|
const onUserStop = (messageIndex: number) => {
|
||||||
|
console.log(ControllerPool, sessionIndex, messageIndex);
|
||||||
|
ControllerPool.stop(sessionIndex, messageIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if should send message
|
||||||
const onInputKeyDown = (e: KeyboardEvent) => {
|
const onInputKeyDown = (e: KeyboardEvent) => {
|
||||||
if (shouldSubmit(e)) {
|
if (shouldSubmit(e)) {
|
||||||
onUserSubmit();
|
onUserSubmit();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const onRightClick = (e: any, message: Message) => {
|
||||||
|
// auto fill user input
|
||||||
|
if (message.role === "user") {
|
||||||
|
setUserInput(message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy to clipboard
|
||||||
|
if (selectOrCopy(e.currentTarget, message.content)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResend = (botIndex: number) => {
|
||||||
|
// find last user input message and resend
|
||||||
|
for (let i = botIndex; i >= 0; i -= 1) {
|
||||||
|
if (messages[i].role === "user") {
|
||||||
|
setIsLoading(true);
|
||||||
|
onUserInput(messages[i].content).then(() => setIsLoading(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// for auto-scroll
|
||||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
const latestMessageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [hoveringMessage, setHoveringMessage] = useState(false);
|
// wont scroll while hovering messages
|
||||||
|
const [autoScroll, setAutoScroll] = useState(false);
|
||||||
|
|
||||||
|
// preview messages
|
||||||
const messages = (session.messages as RenderMessage[])
|
const messages = (session.messages as RenderMessage[])
|
||||||
.concat(
|
.concat(
|
||||||
isLoading
|
isLoading
|
||||||
@@ -194,10 +234,11 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// auto scroll
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const dom = latestMessageRef.current;
|
const dom = latestMessageRef.current;
|
||||||
if (dom && !isIOS() && !hoveringMessage) {
|
if (dom && !isIOS() && autoScroll) {
|
||||||
dom.scrollIntoView({
|
dom.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "end",
|
block: "end",
|
||||||
@@ -252,15 +293,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className={styles["chat-body"]}>
|
||||||
className={styles["chat-body"]}
|
|
||||||
onMouseOver={() => {
|
|
||||||
setHoveringMessage(true);
|
|
||||||
}}
|
|
||||||
onMouseOut={() => {
|
|
||||||
setHoveringMessage(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{messages.map((message, i) => {
|
{messages.map((message, i) => {
|
||||||
const isUser = message.role === "user";
|
const isUser = message.role === "user";
|
||||||
|
|
||||||
@@ -283,13 +316,20 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<div className={styles["chat-message-item"]}>
|
<div className={styles["chat-message-item"]}>
|
||||||
{!isUser && (
|
{!isUser && (
|
||||||
<div className={styles["chat-message-top-actions"]}>
|
<div className={styles["chat-message-top-actions"]}>
|
||||||
{message.streaming && (
|
{message.streaming ? (
|
||||||
<div
|
<div
|
||||||
className={styles["chat-message-top-action"]}
|
className={styles["chat-message-top-action"]}
|
||||||
onClick={() => showToast(Locale.WIP)}
|
onClick={() => onUserStop(i)}
|
||||||
>
|
>
|
||||||
{Locale.Chat.Actions.Stop}
|
{Locale.Chat.Actions.Stop}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onResend(i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Retry}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -306,11 +346,7 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => onRightClick(e, message)}
|
||||||
if (selectOrCopy(e.currentTarget, message.content)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Markdown content={message.content} />
|
<Markdown content={message.content} />
|
||||||
</div>
|
</div>
|
||||||
@@ -341,6 +377,9 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
onInput={(e) => setUserInput(e.currentTarget.value)}
|
onInput={(e) => setUserInput(e.currentTarget.value)}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onKeyDown={(e) => onInputKeyDown(e as any)}
|
onKeyDown={(e) => onInputKeyDown(e as any)}
|
||||||
|
onFocus={() => setAutoScroll(true)}
|
||||||
|
onBlur={() => setAutoScroll(false)}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<SendWhiteIcon />}
|
icon={<SendWhiteIcon />}
|
||||||
|
@@ -14,6 +14,7 @@ const cn = {
|
|||||||
Export: "导出聊天记录",
|
Export: "导出聊天记录",
|
||||||
Copy: "复制",
|
Copy: "复制",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
|
Retry: "重试",
|
||||||
},
|
},
|
||||||
Typing: "正在输入…",
|
Typing: "正在输入…",
|
||||||
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
|
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
|
||||||
|
@@ -17,6 +17,7 @@ const en: LocaleType = {
|
|||||||
Export: "Export All Messages as Markdown",
|
Export: "Export All Messages as Markdown",
|
||||||
Copy: "Copy",
|
Copy: "Copy",
|
||||||
Stop: "Stop",
|
Stop: "Stop",
|
||||||
|
Retry: "Retry",
|
||||||
},
|
},
|
||||||
Typing: "Typing…",
|
Typing: "Typing…",
|
||||||
Input: (submitKey: string) =>
|
Input: (submitKey: string) =>
|
||||||
|
@@ -60,6 +60,7 @@ export async function requestChatStream(
|
|||||||
modelConfig?: ModelConfig;
|
modelConfig?: ModelConfig;
|
||||||
onMessage: (message: string, done: boolean) => void;
|
onMessage: (message: string, done: boolean) => void;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
|
onController?: (controller: AbortController) => void;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const req = makeRequestParam(messages, {
|
const req = makeRequestParam(messages, {
|
||||||
@@ -96,12 +97,12 @@ export async function requestChatStream(
|
|||||||
controller.abort();
|
controller.abort();
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const reader = res.body?.getReader();
|
const reader = res.body?.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
options?.onController?.(controller);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// handle time out, will stop if no response in 10 secs
|
// handle time out, will stop if no response in 10 secs
|
||||||
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
|
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
|
||||||
@@ -146,3 +147,34 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
|
|||||||
|
|
||||||
return res.choices.at(0)?.message?.content ?? "";
|
return res.choices.at(0)?.message?.content ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To store message streaming controller
|
||||||
|
export const ControllerPool = {
|
||||||
|
controllers: {} as Record<string, AbortController>,
|
||||||
|
|
||||||
|
addController(
|
||||||
|
sessionIndex: number,
|
||||||
|
messageIndex: number,
|
||||||
|
controller: AbortController
|
||||||
|
) {
|
||||||
|
const key = this.key(sessionIndex, messageIndex);
|
||||||
|
this.controllers[key] = controller;
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
|
||||||
|
stop(sessionIndex: number, messageIndex: number) {
|
||||||
|
const key = this.key(sessionIndex, messageIndex);
|
||||||
|
const controller = this.controllers[key];
|
||||||
|
console.log(controller);
|
||||||
|
controller?.abort();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(sessionIndex: number, messageIndex: number) {
|
||||||
|
const key = this.key(sessionIndex, messageIndex);
|
||||||
|
delete this.controllers[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
key(sessionIndex: number, messageIndex: number) {
|
||||||
|
return `${sessionIndex},${messageIndex}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@@ -2,7 +2,11 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
import { type ChatCompletionResponseMessage } from "openai";
|
import { type ChatCompletionResponseMessage } from "openai";
|
||||||
import { requestChatStream, requestWithPrompt } from "../requests";
|
import {
|
||||||
|
ControllerPool,
|
||||||
|
requestChatStream,
|
||||||
|
requestWithPrompt,
|
||||||
|
} from "../requests";
|
||||||
import { trimTopic } from "../utils";
|
import { trimTopic } from "../utils";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
@@ -296,6 +300,8 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
// get recent messages
|
// get recent messages
|
||||||
const recentMessages = get().getMessagesWithMemory();
|
const recentMessages = get().getMessagesWithMemory();
|
||||||
const sendMessages = recentMessages.concat(userMessage);
|
const sendMessages = recentMessages.concat(userMessage);
|
||||||
|
const sessionIndex = get().currentSessionIndex;
|
||||||
|
const messageIndex = get().currentSession().messages.length + 1;
|
||||||
|
|
||||||
// save user's and bot's message
|
// save user's and bot's message
|
||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
@@ -303,13 +309,16 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
session.messages.push(botMessage);
|
session.messages.push(botMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// make request
|
||||||
console.log("[User Input] ", sendMessages);
|
console.log("[User Input] ", sendMessages);
|
||||||
requestChatStream(sendMessages, {
|
requestChatStream(sendMessages, {
|
||||||
onMessage(content, done) {
|
onMessage(content, done) {
|
||||||
|
// stream response
|
||||||
if (done) {
|
if (done) {
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
botMessage.content = content;
|
botMessage.content = content;
|
||||||
get().onNewMessage(botMessage);
|
get().onNewMessage(botMessage);
|
||||||
|
ControllerPool.remove(sessionIndex, messageIndex);
|
||||||
} else {
|
} else {
|
||||||
botMessage.content = content;
|
botMessage.content = content;
|
||||||
set(() => ({}));
|
set(() => ({}));
|
||||||
@@ -319,6 +328,15 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
botMessage.content += "\n\n" + Locale.Store.Error;
|
botMessage.content += "\n\n" + Locale.Store.Error;
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
set(() => ({}));
|
set(() => ({}));
|
||||||
|
ControllerPool.remove(sessionIndex, messageIndex);
|
||||||
|
},
|
||||||
|
onController(controller) {
|
||||||
|
// collect controller for stop/retry
|
||||||
|
ControllerPool.addController(
|
||||||
|
sessionIndex,
|
||||||
|
messageIndex,
|
||||||
|
controller
|
||||||
|
);
|
||||||
},
|
},
|
||||||
filterBot: !get().config.sendBotMessages,
|
filterBot: !get().config.sendBotMessages,
|
||||||
modelConfig: get().config.modelConfig,
|
modelConfig: get().config.modelConfig,
|
||||||
|
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 633 B |
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 15 KiB |