fix: resolve conflict
							
								
								
									
										18
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
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://user-images.githubusercontent.com/16968934/227772522-b3ba3713-9206-4c8d-a81f-22300b7c313a.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
 | 
			
		||||
 | 
			
		||||
[](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)
 | 
			
		||||
 | 
			
		||||
@@ -84,7 +84,7 @@ You can star or watch this project or follow author to get release notifictions
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||
@@ -109,18 +109,22 @@ OPENAI_API_KEY=<your api key here>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 本地开发 Local Development
 | 
			
		||||
 | 
			
		||||
> 如果你是中国大陆用户,不建议在本地进行开发,除非你能够独立解决 OpenAI API 本地代理问题。
 | 
			
		||||
 | 
			
		||||
1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT;
 | 
			
		||||
2. 执行 `yarn install && yarn dev` 即可。
 | 
			
		||||
 | 
			
		||||
### 本地部署 Local Deployment
 | 
			
		||||
 | 
			
		||||
请直接询问 ChatGPT,使用下列 Prompt:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
如何使用 pm2 和 yarn 部署 nextjs 项目到 ubuntu 服务器上,项目编译命令为 yarn build,启动命令为 yarn start,启动时需要设置环境变量为 OPENAI_API_KEY,端口为 3000,使用 ngnix 做反向代理
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Please ask ChatGPT with prompt:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
how to deploy nextjs project with pm2 and yarn on my ubuntu server, the build command is `yarn build`, the start command is `yarn start`, the project must start with env var named `OPENAI_API_KEY`, the port is 3000, use ngnix
 | 
			
		||||
```
 | 
			
		||||
@@ -157,14 +161,22 @@ Please replace `OPEN_API_KEY` and `CODE` with your own credentials at the approp
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 说明 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. 
 | 
			
		||||
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)
 | 
			
		||||
[@ClarenceDan](https://github.com/ClarenceDan)
 | 
			
		||||
 | 
			
		||||
## LICENSE
 | 
			
		||||
 | 
			
		||||
- [Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,19 +2,25 @@ import type { ChatRequest } from "../chat/typing";
 | 
			
		||||
import { createParser } from "eventsource-parser";
 | 
			
		||||
import { NextRequest } from "next/server";
 | 
			
		||||
 | 
			
		||||
const apiKey = process.env.OPENAI_API_KEY;
 | 
			
		||||
 | 
			
		||||
async function createStream(payload: ReadableStream<Uint8Array>) {
 | 
			
		||||
async function createStream(req: NextRequest) {
 | 
			
		||||
  const encoder = new TextEncoder();
 | 
			
		||||
  const decoder = new TextDecoder();
 | 
			
		||||
 | 
			
		||||
  let apiKey = process.env.OPENAI_API_KEY;
 | 
			
		||||
 | 
			
		||||
  const userApiKey = req.headers.get("token");
 | 
			
		||||
  if (userApiKey) {
 | 
			
		||||
    apiKey = userApiKey;
 | 
			
		||||
    console.log("[Stream] using user api key");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
      Authorization: `Bearer ${apiKey}`,
 | 
			
		||||
    },
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    body: payload,
 | 
			
		||||
    body: req.body,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const stream = new ReadableStream({
 | 
			
		||||
@@ -49,7 +55,7 @@ async function createStream(payload: ReadableStream<Uint8Array>) {
 | 
			
		||||
 | 
			
		||||
export async function POST(req: NextRequest) {
 | 
			
		||||
  try {
 | 
			
		||||
    const stream = await createStream(req.body!);
 | 
			
		||||
    const stream = await createStream(req);
 | 
			
		||||
    return new Response(stream);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("[Chat Stream]", error);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,26 @@
 | 
			
		||||
import { OpenAIApi, Configuration } from "openai";
 | 
			
		||||
import { ChatRequest } from "./typing";
 | 
			
		||||
 | 
			
		||||
const apiKey = process.env.OPENAI_API_KEY;
 | 
			
		||||
 | 
			
		||||
const openai = new OpenAIApi(
 | 
			
		||||
  new Configuration({
 | 
			
		||||
    apiKey,
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export async function POST(req: Request) {
 | 
			
		||||
  try {
 | 
			
		||||
    const requestBody = (await req.json()) as ChatRequest;
 | 
			
		||||
    const completion = await openai!.createChatCompletion(
 | 
			
		||||
      {
 | 
			
		||||
        ...requestBody,
 | 
			
		||||
      }
 | 
			
		||||
    let apiKey = process.env.OPENAI_API_KEY;
 | 
			
		||||
 | 
			
		||||
    const userApiKey = req.headers.get("token");
 | 
			
		||||
    if (userApiKey) {
 | 
			
		||||
      apiKey = userApiKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const openai = new OpenAIApi(
 | 
			
		||||
      new Configuration({
 | 
			
		||||
        apiKey,
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const requestBody = (await req.json()) as ChatRequest;
 | 
			
		||||
    const completion = await openai!.createChatCompletion({
 | 
			
		||||
      ...requestBody,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return new Response(JSON.stringify(completion.data));
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error("[Chat] ", e);
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ import Locale from "../locales";
 | 
			
		||||
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import { REPO_URL } from "../constant";
 | 
			
		||||
import { ControllerPool } from "../requests";
 | 
			
		||||
 | 
			
		||||
export function Loading(props: { noLogo?: boolean }) {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -146,28 +147,67 @@ function useSubmitHandler() {
 | 
			
		||||
export function Chat(props: { showSideBar?: () => void }) {
 | 
			
		||||
  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 [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const { submitKey, shouldSubmit } = useSubmitHandler();
 | 
			
		||||
 | 
			
		||||
  const onUserInput = useChatStore((state) => state.onUserInput);
 | 
			
		||||
 | 
			
		||||
  // submit user input
 | 
			
		||||
  const onUserSubmit = () => {
 | 
			
		||||
    if (userInput.length <= 0) return;
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    onUserInput(userInput).then(() => setIsLoading(false));
 | 
			
		||||
    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) => {
 | 
			
		||||
    if (shouldSubmit(e)) {
 | 
			
		||||
      onUserSubmit();
 | 
			
		||||
      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 [hoveringMessage, setHoveringMessage] = useState(false);
 | 
			
		||||
  // wont scroll while hovering messages
 | 
			
		||||
  const [autoScroll, setAutoScroll] = useState(false);
 | 
			
		||||
 | 
			
		||||
  // preview messages
 | 
			
		||||
  const messages = (session.messages as RenderMessage[])
 | 
			
		||||
    .concat(
 | 
			
		||||
      isLoading
 | 
			
		||||
@@ -194,10 +234,11 @@ export function Chat(props: { showSideBar?: () => void }) {
 | 
			
		||||
        : []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  // auto scroll
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      const dom = latestMessageRef.current;
 | 
			
		||||
      if (dom && !isIOS() && !hoveringMessage) {
 | 
			
		||||
      if (dom && !isIOS() && autoScroll) {
 | 
			
		||||
        dom.scrollIntoView({
 | 
			
		||||
          behavior: "smooth",
 | 
			
		||||
          block: "end",
 | 
			
		||||
@@ -252,15 +293,7 @@ export function Chat(props: { showSideBar?: () => void }) {
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        className={styles["chat-body"]}
 | 
			
		||||
        onMouseOver={() => {
 | 
			
		||||
          setHoveringMessage(true);
 | 
			
		||||
        }}
 | 
			
		||||
        onMouseOut={() => {
 | 
			
		||||
          setHoveringMessage(false);
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
      <div className={styles["chat-body"]}>
 | 
			
		||||
        {messages.map((message, i) => {
 | 
			
		||||
          const isUser = message.role === "user";
 | 
			
		||||
 | 
			
		||||
@@ -283,13 +316,20 @@ export function Chat(props: { showSideBar?: () => void }) {
 | 
			
		||||
                <div className={styles["chat-message-item"]}>
 | 
			
		||||
                  {!isUser && (
 | 
			
		||||
                    <div className={styles["chat-message-top-actions"]}>
 | 
			
		||||
                      {message.streaming && (
 | 
			
		||||
                      {message.streaming ? (
 | 
			
		||||
                        <div
 | 
			
		||||
                          className={styles["chat-message-top-action"]}
 | 
			
		||||
                          onClick={() => showToast(Locale.WIP)}
 | 
			
		||||
                          onClick={() => onUserStop(i)}
 | 
			
		||||
                        >
 | 
			
		||||
                          {Locale.Chat.Actions.Stop}
 | 
			
		||||
                        </div>
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <div
 | 
			
		||||
                          className={styles["chat-message-top-action"]}
 | 
			
		||||
                          onClick={() => onResend(i)}
 | 
			
		||||
                        >
 | 
			
		||||
                          {Locale.Chat.Actions.Retry}
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )}
 | 
			
		||||
 | 
			
		||||
                      <div
 | 
			
		||||
@@ -306,11 +346,7 @@ export function Chat(props: { showSideBar?: () => void }) {
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <div
 | 
			
		||||
                      className="markdown-body"
 | 
			
		||||
                      onContextMenu={(e) => {
 | 
			
		||||
                        if (selectOrCopy(e.currentTarget, message.content)) {
 | 
			
		||||
                          e.preventDefault();
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                      onContextMenu={(e) => onRightClick(e, message)}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Markdown content={message.content} />
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -341,6 +377,9 @@ export function Chat(props: { showSideBar?: () => void }) {
 | 
			
		||||
            onInput={(e) => setUserInput(e.currentTarget.value)}
 | 
			
		||||
            value={userInput}
 | 
			
		||||
            onKeyDown={(e) => onInputKeyDown(e as any)}
 | 
			
		||||
            onFocus={() => setAutoScroll(true)}
 | 
			
		||||
            onBlur={() => setAutoScroll(false)}
 | 
			
		||||
            autoFocus
 | 
			
		||||
          />
 | 
			
		||||
          <IconButton
 | 
			
		||||
            icon={<SendWhiteIcon />}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,36 @@ import RemarkMath from "remark-math";
 | 
			
		||||
import RehypeKatex from "rehype-katex";
 | 
			
		||||
import RemarkGfm from "remark-gfm";
 | 
			
		||||
import RehypePrsim from "rehype-prism-plus";
 | 
			
		||||
import { useRef } from "react";
 | 
			
		||||
import { copyToClipboard } from "../utils";
 | 
			
		||||
 | 
			
		||||
export function PreCode(props: { children: any }) {
 | 
			
		||||
  const ref = useRef<HTMLPreElement>(null);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <pre ref={ref}>
 | 
			
		||||
      <span
 | 
			
		||||
        className="copy-code-button"
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          if (ref.current) {
 | 
			
		||||
            const code = ref.current.innerText;
 | 
			
		||||
            copyToClipboard(code);
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      ></span>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </pre>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Markdown(props: { content: string }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ReactMarkdown
 | 
			
		||||
      remarkPlugins={[RemarkMath, RemarkGfm]}
 | 
			
		||||
      rehypePlugins={[
 | 
			
		||||
        RehypeKatex,
 | 
			
		||||
        [RehypePrsim, { ignoreMissing: true }],
 | 
			
		||||
      ]}
 | 
			
		||||
      rehypePlugins={[RehypeKatex, [RehypePrsim, { ignoreMissing: true }]]}
 | 
			
		||||
      components={{
 | 
			
		||||
        pre: PreCode,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {props.content}
 | 
			
		||||
    </ReactMarkdown>
 | 
			
		||||
 
 | 
			
		||||
@@ -257,6 +257,20 @@ export function Settings(props: { closeSettings: () => void }) {
 | 
			
		||||
            <></>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
            title={Locale.Settings.Token.Title}
 | 
			
		||||
            subTitle={Locale.Settings.Token.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              value={accessStore.token}
 | 
			
		||||
              type="text"
 | 
			
		||||
              placeholder={Locale.Settings.Token.Placeholder}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
                accessStore.updateToken(e.currentTarget.value);
 | 
			
		||||
              }}
 | 
			
		||||
            ></input>
 | 
			
		||||
          </SettingItem>
 | 
			
		||||
 | 
			
		||||
          <SettingItem
 | 
			
		||||
            title={Locale.Settings.HistoryCount.Title}
 | 
			
		||||
            subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ const cn = {
 | 
			
		||||
      Export: "导出聊天记录",
 | 
			
		||||
      Copy: "复制",
 | 
			
		||||
      Stop: "停止",
 | 
			
		||||
      Retry: "重试",
 | 
			
		||||
    },
 | 
			
		||||
    Typing: "正在输入…",
 | 
			
		||||
    Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
 | 
			
		||||
@@ -68,6 +69,11 @@ const cn = {
 | 
			
		||||
      Title: "历史消息长度压缩阈值",
 | 
			
		||||
      SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
 | 
			
		||||
    },
 | 
			
		||||
    Token: {
 | 
			
		||||
      Title: "API Key",
 | 
			
		||||
      SubTitle: "使用自己的 Key 可绕过受控访问限制",
 | 
			
		||||
      Placeholder: "OpenAI API Key",
 | 
			
		||||
    },
 | 
			
		||||
    AccessCode: {
 | 
			
		||||
      Title: "访问码",
 | 
			
		||||
      SubTitle: "现在是受控访问状态",
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ const en: LocaleType = {
 | 
			
		||||
      Export: "Export All Messages as Markdown",
 | 
			
		||||
      Copy: "Copy",
 | 
			
		||||
      Stop: "Stop",
 | 
			
		||||
      Retry: "Retry",
 | 
			
		||||
    },
 | 
			
		||||
    Typing: "Typing…",
 | 
			
		||||
    Input: (submitKey: string) =>
 | 
			
		||||
@@ -73,6 +74,11 @@ const en: LocaleType = {
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Will compress if uncompressed messages length exceeds the value",
 | 
			
		||||
    },
 | 
			
		||||
    Token: {
 | 
			
		||||
      Title: "API Key",
 | 
			
		||||
      SubTitle: "Use your key to ignore access code limit",
 | 
			
		||||
      Placeholder: "OpenAI API Key",
 | 
			
		||||
    },
 | 
			
		||||
    AccessCode: {
 | 
			
		||||
      Title: "Access Code",
 | 
			
		||||
      SubTitle: "Access control enabled",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { Analytics } from "@vercel/analytics/react";
 | 
			
		||||
import { Home } from './components/home'
 | 
			
		||||
import { Home } from "./components/home";
 | 
			
		||||
 | 
			
		||||
export default function App() {
 | 
			
		||||
  return (
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,10 @@ function getHeaders() {
 | 
			
		||||
    headers["access-code"] = accessStore.accessCode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (accessStore.token && accessStore.token.length > 0) {
 | 
			
		||||
    headers["token"] = accessStore.token;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return headers;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -60,6 +64,7 @@ export async function requestChatStream(
 | 
			
		||||
    modelConfig?: ModelConfig;
 | 
			
		||||
    onMessage: (message: string, done: boolean) => void;
 | 
			
		||||
    onError: (error: Error) => void;
 | 
			
		||||
    onController?: (controller: AbortController) => void;
 | 
			
		||||
  }
 | 
			
		||||
) {
 | 
			
		||||
  const req = makeRequestParam(messages, {
 | 
			
		||||
@@ -96,12 +101,12 @@ export async function requestChatStream(
 | 
			
		||||
      controller.abort();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    console.log(res);
 | 
			
		||||
 | 
			
		||||
    if (res.ok) {
 | 
			
		||||
      const reader = res.body?.getReader();
 | 
			
		||||
      const decoder = new TextDecoder();
 | 
			
		||||
 | 
			
		||||
      options?.onController?.(controller);
 | 
			
		||||
 | 
			
		||||
      while (true) {
 | 
			
		||||
        // handle time out, will stop if no response in 10 secs
 | 
			
		||||
        const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
 | 
			
		||||
@@ -146,3 +151,34 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
 | 
			
		||||
 | 
			
		||||
  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}`;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@ import { queryMeta } from "../utils";
 | 
			
		||||
 | 
			
		||||
export interface AccessControlStore {
 | 
			
		||||
  accessCode: string;
 | 
			
		||||
  token: string;
 | 
			
		||||
 | 
			
		||||
  updateToken: (_: string) => void;
 | 
			
		||||
  updateCode: (_: string) => void;
 | 
			
		||||
  enabledAccessControl: () => boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +16,7 @@ export const ACCESS_KEY = "access-control";
 | 
			
		||||
export const useAccessStore = create<AccessControlStore>()(
 | 
			
		||||
  persist(
 | 
			
		||||
    (set, get) => ({
 | 
			
		||||
      token: "",
 | 
			
		||||
      accessCode: "",
 | 
			
		||||
      enabledAccessControl() {
 | 
			
		||||
        return queryMeta("access") === "enabled";
 | 
			
		||||
@@ -21,6 +24,9 @@ export const useAccessStore = create<AccessControlStore>()(
 | 
			
		||||
      updateCode(code: string) {
 | 
			
		||||
        set((state) => ({ accessCode: code }));
 | 
			
		||||
      },
 | 
			
		||||
      updateToken(token: string) {
 | 
			
		||||
        set((state) => ({ token }));
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    {
 | 
			
		||||
      name: ACCESS_KEY,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,11 @@ import { create } from "zustand";
 | 
			
		||||
import { persist } from "zustand/middleware";
 | 
			
		||||
 | 
			
		||||
import { type ChatCompletionResponseMessage } from "openai";
 | 
			
		||||
import { requestChatStream, requestWithPrompt } from "../requests";
 | 
			
		||||
import {
 | 
			
		||||
  ControllerPool,
 | 
			
		||||
  requestChatStream,
 | 
			
		||||
  requestWithPrompt,
 | 
			
		||||
} from "../requests";
 | 
			
		||||
import { trimTopic } from "../utils";
 | 
			
		||||
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
@@ -45,22 +49,24 @@ export interface ChatConfig {
 | 
			
		||||
 | 
			
		||||
export type ModelConfig = ChatConfig["modelConfig"];
 | 
			
		||||
 | 
			
		||||
const ENABLE_GPT4 = true;
 | 
			
		||||
 | 
			
		||||
export const ALL_MODELS = [
 | 
			
		||||
  {
 | 
			
		||||
    name: "gpt-4",
 | 
			
		||||
    available: false,
 | 
			
		||||
    available: ENABLE_GPT4,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "gpt-4-0314",
 | 
			
		||||
    available: false,
 | 
			
		||||
    available: ENABLE_GPT4,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "gpt-4-32k",
 | 
			
		||||
    available: false,
 | 
			
		||||
    available: ENABLE_GPT4,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "gpt-4-32k-0314",
 | 
			
		||||
    available: false,
 | 
			
		||||
    available: ENABLE_GPT4,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "gpt-3.5-turbo",
 | 
			
		||||
@@ -296,6 +302,8 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
        // get recent messages
 | 
			
		||||
        const recentMessages = get().getMessagesWithMemory();
 | 
			
		||||
        const sendMessages = recentMessages.concat(userMessage);
 | 
			
		||||
        const sessionIndex = get().currentSessionIndex;
 | 
			
		||||
        const messageIndex = get().currentSession().messages.length + 1;
 | 
			
		||||
 | 
			
		||||
        // save user's and bot's message
 | 
			
		||||
        get().updateCurrentSession((session) => {
 | 
			
		||||
@@ -303,13 +311,16 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
          session.messages.push(botMessage);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // make request
 | 
			
		||||
        console.log("[User Input] ", sendMessages);
 | 
			
		||||
        requestChatStream(sendMessages, {
 | 
			
		||||
          onMessage(content, done) {
 | 
			
		||||
            // stream response
 | 
			
		||||
            if (done) {
 | 
			
		||||
              botMessage.streaming = false;
 | 
			
		||||
              botMessage.content = content;
 | 
			
		||||
              get().onNewMessage(botMessage);
 | 
			
		||||
              ControllerPool.remove(sessionIndex, messageIndex);
 | 
			
		||||
            } else {
 | 
			
		||||
              botMessage.content = content;
 | 
			
		||||
              set(() => ({}));
 | 
			
		||||
@@ -319,6 +330,15 @@ export const useChatStore = create<ChatStore>()(
 | 
			
		||||
            botMessage.content += "\n\n" + Locale.Store.Error;
 | 
			
		||||
            botMessage.streaming = false;
 | 
			
		||||
            set(() => ({}));
 | 
			
		||||
            ControllerPool.remove(sessionIndex, messageIndex);
 | 
			
		||||
          },
 | 
			
		||||
          onController(controller) {
 | 
			
		||||
            // collect controller for stop/retry
 | 
			
		||||
            ControllerPool.addController(
 | 
			
		||||
              sessionIndex,
 | 
			
		||||
              messageIndex,
 | 
			
		||||
              controller
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
          filterBot: !get().config.sendBotMessages,
 | 
			
		||||
          modelConfig: get().config.modelConfig,
 | 
			
		||||
 
 | 
			
		||||
@@ -206,3 +206,36 @@ div.math {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pre {
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  &:hover .copy-code-button {
 | 
			
		||||
    pointer-events: all;
 | 
			
		||||
    transform: translateX(0px);
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .copy-code-button {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 10px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 0px 5px;
 | 
			
		||||
    background-color: var(--black);
 | 
			
		||||
    color: var(--white);
 | 
			
		||||
    border: var(--border-in-light);
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    transform: translateX(10px);
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transition: all ease 0.3s;
 | 
			
		||||
 | 
			
		||||
    &:after {
 | 
			
		||||
      content: "copy";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,9 @@
 | 
			
		||||
.markdown-body {
 | 
			
		||||
  pre {
 | 
			
		||||
    background: #282a36;
 | 
			
		||||
    color: #f8f8f2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  code[class*="language-"],
 | 
			
		||||
  pre[class*="language-"] {
 | 
			
		||||
    color: #f8f8f2;
 | 
			
		||||
@@ -116,32 +121,32 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin light {
 | 
			
		||||
  .markdown-body pre[class*="language-"] {
 | 
			
		||||
    filter: invert(1) hue-rotate(50deg) brightness(1.3);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// @mixin light {
 | 
			
		||||
//   .markdown-body pre[class*="language-"] {
 | 
			
		||||
//     filter: invert(1) hue-rotate(50deg) brightness(1.3);
 | 
			
		||||
//   }
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
@mixin dark {
 | 
			
		||||
  .markdown-body pre[class*="language-"] {
 | 
			
		||||
    filter: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// @mixin dark {
 | 
			
		||||
//   .markdown-body pre[class*="language-"] {
 | 
			
		||||
//     filter: none;
 | 
			
		||||
//   }
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  @include light();
 | 
			
		||||
}
 | 
			
		||||
// :root {
 | 
			
		||||
// @include light();
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
.light {
 | 
			
		||||
  @include light();
 | 
			
		||||
}
 | 
			
		||||
// .light {
 | 
			
		||||
//   @include light();
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
.dark {
 | 
			
		||||
  @include dark();
 | 
			
		||||
}
 | 
			
		||||
// .dark {
 | 
			
		||||
//   @include dark();
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  :root {
 | 
			
		||||
    @include dark();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// @media (prefers-color-scheme: dark) {
 | 
			
		||||
//   :root {
 | 
			
		||||
//     @include dark();
 | 
			
		||||
//   }
 | 
			
		||||
// }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,14 @@ export const config = {
 | 
			
		||||
 | 
			
		||||
export function middleware(req: NextRequest, res: NextResponse) {
 | 
			
		||||
  const accessCode = req.headers.get("access-code");
 | 
			
		||||
  const token = req.headers.get("token");
 | 
			
		||||
  const hashedCode = md5.hash(accessCode ?? "").trim();
 | 
			
		||||
 | 
			
		||||
  console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
 | 
			
		||||
  console.log("[Auth] got access code:", accessCode);
 | 
			
		||||
  console.log("[Auth] hashed access code:", hashedCode);
 | 
			
		||||
 | 
			
		||||
  if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode)) {
 | 
			
		||||
  if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
 | 
			
		||||
    return NextResponse.json(
 | 
			
		||||
      {
 | 
			
		||||
        needAccessCode: true,
 | 
			
		||||
 
 | 
			
		||||
| 
		 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  |