From e6068105816f0a45d50e80efda0f40787ceaa6c8 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 15:31:55 +0000 Subject: [PATCH 01/11] refactor: #121 trigger auto-cmp with / prefix --- app/components/home.tsx | 100 ++++++++++++++++++++++++---------------- app/locales/cn.ts | 2 +- app/locales/en.ts | 2 +- app/locales/tw.ts | 7 ++- 4 files changed, 64 insertions(+), 47 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 909e5406..341054b8 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -102,7 +102,7 @@ export function ChatList() { state.currentSessionIndex, state.selectSession, state.removeSession, - ] + ], ); return ( @@ -128,7 +128,7 @@ function useSubmitHandler() { const shouldSubmit = (e: KeyboardEvent) => { if (e.key !== "Enter") return false; - + return ( (config.submitKey === SubmitKey.AltEnter && e.altKey) || (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) || @@ -170,7 +170,10 @@ export function PromptHints(props: { ); } -export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean }) { +export function Chat(props: { + showSideBar?: () => void; + sideBarShowing?: boolean; +}) { type RenderMessage = Message & { preview?: boolean }; const chatStore = useChatStore(); @@ -190,11 +193,10 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean const [promptHints, setPromptHints] = useState([]); const onSearch = useDebouncedCallback( (text: string) => { - if (chatStore.config.disablePromptHint) return; setPromptHints(promptStore.search(text)); }, 100, - { leading: true, trailing: true } + { leading: true, trailing: true }, ); const onPromptSelect = (prompt: Prompt) => { @@ -203,20 +205,31 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean inputRef.current?.focus(); }; + const scrollInput = () => { + const dom = inputRef.current; + if (!dom) return; + const paddingBottomNum: number = parseInt( + window.getComputedStyle(dom).paddingBottom, + 10, + ); + dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; + }; + // only search prompts when user input is short const SEARCH_TEXT_LIMIT = 30; const onInput = (text: string) => { - const textareaDom = inputRef.current - if (textareaDom) { - const paddingBottomNum: number = parseInt(window.getComputedStyle(textareaDom).paddingBottom, 10); - textareaDom.scrollTop = textareaDom.scrollHeight - textareaDom.offsetHeight + paddingBottomNum; - } + scrollInput(); setUserInput(text); const n = text.trim().length; - if (n === 0 || n > SEARCH_TEXT_LIMIT) { + + // clear search results + if (n === 0) { setPromptHints([]); - } else { - onSearch(text); + } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { + // check if need to trigger auto completion + if (text.startsWith("/") && text.length > 1) { + onSearch(text.slice(1)); + } } }; @@ -285,7 +298,7 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean preview: true, }, ] - : [] + : [], ) .concat( userInput.length > 0 @@ -297,7 +310,7 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean preview: true, }, ] - : [] + : [], ); // auto scroll @@ -380,32 +393,33 @@ export function Chat(props: { showSideBar?: () => void, sideBarShowing?: boolean )}
- {(!isUser && !(message.preview || message.content.length === 0)) && ( -
- {message.streaming ? ( -
onUserStop(i)} - > - {Locale.Chat.Actions.Stop} -
- ) : ( -
onResend(i)} - > - {Locale.Chat.Actions.Retry} -
- )} + {!isUser && + !(message.preview || message.content.length === 0) && ( +
+ {message.streaming ? ( +
onUserStop(i)} + > + {Locale.Chat.Actions.Stop} +
+ ) : ( +
onResend(i)} + > + {Locale.Chat.Actions.Retry} +
+ )} -
copyToClipboard(message.content)} - > - {Locale.Chat.Actions.Copy} +
copyToClipboard(message.content)} + > + {Locale.Chat.Actions.Copy} +
-
- )} + )} {(message.preview || message.content.length === 0) && !isUser ? ( @@ -560,7 +574,7 @@ export function Home() { state.newSession, state.currentSessionIndex, state.removeSession, - ] + ], ); const loading = !useHasHydrated(); const [showSideBar, setShowSideBar] = useState(true); @@ -653,7 +667,11 @@ export function Home() { }} /> ) : ( - setShowSideBar(true)} sideBarShowing={showSideBar} /> + setShowSideBar(true)} + sideBarShowing={showSideBar} + /> )}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts index b93ec859..76f6a770 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -77,7 +77,7 @@ const cn = { Prompt: { Disable: { Title: "禁用提示词自动补全", - SubTitle: "禁用后将无法自动根据输入补全", + SubTitle: "在输入框开头输入 / 即可触发自动补全", }, List: "自定义提示词列表", ListCount: (builtin: number, custom: number) => diff --git a/app/locales/en.ts b/app/locales/en.ts index d1689372..d21679ab 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -79,7 +79,7 @@ const en: LocaleType = { Prompt: { Disable: { Title: "Disable auto-completion", - SubTitle: "After disabling, auto-completion will not be available", + SubTitle: "Input / to trigger auto-completion", }, List: "Prompt List", ListCount: (builtin: number, custom: number) => diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 29f5ec22..63312eb4 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -20,7 +20,7 @@ const tw: LocaleType = { Retry: "重試", }, Typing: "正在輸入…", - Input: (submitKey: string) => { + Input: (submitKey: string) => { var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`; if (submitKey === String(SubmitKey.Enter)) { inputHints += ",Shift + Enter 鍵換行"; @@ -78,7 +78,7 @@ const tw: LocaleType = { Prompt: { Disable: { Title: "停用提示詞自動補全", - SubTitle: "若停用後,將無法自動根據輸入進行補全", + SubTitle: "在輸入框開頭輸入 / 即可觸發自動補全", }, List: "自定義提示詞列表", ListCount: (builtin: number, custom: number) => @@ -124,8 +124,7 @@ const tw: LocaleType = { Prompt: { History: (content: string) => "這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content, - Topic: - "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」", + Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」", Summarize: "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內", }, From 08f3c7026d07bcf28d278dd482d6ac30b8fe3fe4 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 15:40:37 +0000 Subject: [PATCH 02/11] feat: #170 auto scroll after retrying --- app/components/home.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 341054b8..de2cc8fe 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -275,6 +275,7 @@ export function Chat(props: { chatStore .onUserInput(messages[i].content) .then(() => setIsLoading(false)); + inputRef.current?.focus(); return; } } @@ -319,7 +320,6 @@ export function Chat(props: { const dom = latestMessageRef.current; if (dom && !isIOS() && autoScroll) { dom.scrollIntoView({ - behavior: "smooth", block: "end", }); } @@ -444,7 +444,7 @@ export function Chat(props: { ); })} -
+
-
From 45088a3e0658beac56251ff2d4cebc8dc2c5becc Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 16:02:50 +0000 Subject: [PATCH 03/11] feat: #112 add edit chat title --- .lintstagedrc.json | 10 +++++----- app/components/home.module.scss | 8 ++++++++ app/components/home.tsx | 12 +++++++++++- app/locales/cn.ts | 3 ++- app/locales/en.ts | 3 ++- app/locales/tw.ts | 1 + app/store/app.ts | 17 +++++++++++------ 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 023bf16a..58784bad 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,6 +1,6 @@ { - "./app/**/*.{js,ts,jsx,tsx,json,html,css,scss,md}": [ - "eslint --fix", - "prettier --write" - ] -} \ No newline at end of file + "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [ + "eslint --fix", + "prettier --write" + ] +} diff --git a/app/components/home.module.scss b/app/components/home.module.scss index fb96bd45..87231fee 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -221,6 +221,14 @@ margin-bottom: 100px; } +.chat-body-title { + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + .chat-message { display: flex; flex-direction: row; diff --git a/app/components/home.tsx b/app/components/home.tsx index de2cc8fe..847b6cf4 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -333,7 +333,17 @@ export function Chat(props: { className={styles["window-header-title"]} onClick={props?.showSideBar} > -
+
{ + const newTopic = prompt(Locale.Chat.Rename, session.topic); + if (newTopic && newTopic !== session.topic) { + chatStore.updateCurrentSession( + (session) => (session.topic = newTopic!), + ); + } + }} + > {session.topic}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 76f6a770..e9cbad9e 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -18,6 +18,7 @@ const cn = { Stop: "停止", Retry: "重试", }, + Rename: "重命名对话", Typing: "正在输入…", Input: (submitKey: string) => { var inputHints = `输入消息,${submitKey} 发送`; @@ -124,7 +125,7 @@ const cn = { History: (content: string) => "这是 ai 和用户的历史聊天总结作为前情提要:" + content, Topic: - "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”", + "使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”", Summarize: "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内", }, diff --git a/app/locales/en.ts b/app/locales/en.ts index d21679ab..f31badd0 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -20,6 +20,7 @@ const en: LocaleType = { Stop: "Stop", Retry: "Retry", }, + Rename: "Rename Chat", Typing: "Typing…", Input: (submitKey: string) => { var inputHints = `Type something and press ${submitKey} to send`; @@ -129,7 +130,7 @@ const en: LocaleType = { "This is a summary of the chat history between the AI and the user as a recap: " + content, Topic: - "Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.", + "Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.", Summarize: "Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.", }, diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 63312eb4..b78e1b83 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -19,6 +19,7 @@ const tw: LocaleType = { Stop: "停止", Retry: "重試", }, + Rename: "重命名對話", Typing: "正在輸入…", Input: (submitKey: string) => { var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`; diff --git a/app/store/app.ts b/app/store/app.ts index 8a978c0a..118e9ed6 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -206,6 +206,10 @@ interface ChatStore { clearAllData: () => void; } +function countMessages(msgs: Message[]) { + return msgs.reduce((pre, cur) => pre + cur.content.length, 0); +} + const LOCAL_KEY = "chat-next-web-store"; export const useChatStore = create()( @@ -393,8 +397,12 @@ export const useChatStore = create()( summarizeSession() { const session = get().currentSession(); - if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) { - // should summarize topic + // should summarize topic after chating more than 50 words + const SUMMARIZE_MIN_LEN = 50; + if ( + session.topic === DEFAULT_TOPIC && + countMessages(session.messages) >= SUMMARIZE_MIN_LEN + ) { requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then( (res) => { get().updateCurrentSession( @@ -408,10 +416,7 @@ export const useChatStore = create()( let toBeSummarizedMsgs = session.messages.slice( session.lastSummarizeIndex, ); - const historyMsgLength = toBeSummarizedMsgs.reduce( - (pre, cur) => pre + cur.content.length, - 0, - ); + const historyMsgLength = countMessages(toBeSummarizedMsgs); if (historyMsgLength > 4000) { toBeSummarizedMsgs = toBeSummarizedMsgs.slice( From 447dec9444c61f6caf23008a17bd7ad5e2e445c5 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 17:45:26 +0000 Subject: [PATCH 04/11] feat: close #2 add check account balance --- README.md | 41 +++++++++++++++-- app/api/chat-stream/route.ts | 18 +------- app/api/chat/.gitignore | 1 - app/api/chat/route.ts | 29 ------------ app/api/common.ts | 22 +++++++++ app/api/openai/route.ts | 28 ++++++++++++ app/api/{chat => openai}/typing.ts | 0 app/components/settings.tsx | 72 ++++++++++++++++++++++++------ app/locales/cn.ts | 9 ++++ app/locales/en.ts | 8 ++++ app/locales/tw.ts | 8 ++++ app/requests.ts | 60 ++++++++++++++++++------- middleware.ts | 25 +++++++++-- public/serviceWorker.js | 23 +++------- 14 files changed, 245 insertions(+), 99 deletions(-) delete mode 100644 app/api/chat/.gitignore delete mode 100644 app/api/chat/route.ts create mode 100644 app/api/common.ts create mode 100644 app/api/openai/route.ts rename app/api/{chat => openai}/typing.ts (100%) diff --git a/README.md b/README.md index b18028d7..022fada7 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,9 @@ This project will be continuously maintained. If you want to keep the code repos You can star or watch this project or follow author to get release notifictions in time. -## 访问控制 Access Control +## 配置密码 Password -本项目提供有限的权限控制功能,请在环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义控制码: +本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码: ``` code1,code2,code3 @@ -88,7 +88,7 @@ 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 vercel environment variables page. The value should be passwords separated by comma like this: ``` code1,code2,code3 @@ -96,6 +96,38 @@ code1,code2,code3 After adding or modifying this environment variable, please redeploy the project for the changes to take effect. +## 环境变量 Environment Variables + +### `OPENAI_API_KEY` (required) + +OpanAI 密钥。 + +Your openai api key. + +### `CODE` (optional) + +访问密码,可选,可以使用逗号隔开多个密码。 + +Access passsword, separated by comma. + +### `BASE_URL` (optional) + +> Default: `api.openai.com` + +OpenAI 接口代理 URL。 + +Override openai api request base url. + +### `PROTOCOL` (optional) + +> Default: `https` + +> Values: `http` | `https` + +OpenAI 接口协议。 + +Override openai api request protocol. + ## 开发 Development 点击下方按钮,开始二次开发: @@ -118,11 +150,11 @@ OPENAI_API_KEY= 2. 执行 `yarn install && yarn dev` 即可。 ### 本地部署 Local Deployment + ```shell bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` - ### 容器部署 Docker Deployment ```shell @@ -157,6 +189,7 @@ If you would like to contribute your API key, you can email it to the author and [@hoochanlon](https://github.com/hoochanlon) ### 贡献者 Contributor + [Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors) ## LICENSE diff --git a/app/api/chat-stream/route.ts b/app/api/chat-stream/route.ts index ad40c6be..e7bdfc5f 100644 --- a/app/api/chat-stream/route.ts +++ b/app/api/chat-stream/route.ts @@ -1,26 +1,12 @@ import { createParser } from "eventsource-parser"; import { NextRequest } from "next/server"; +import { requestOpenai } from "../common"; 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: req.body, - }); + const res = await requestOpenai(req); const stream = new ReadableStream({ async start(controller) { diff --git a/app/api/chat/.gitignore b/app/api/chat/.gitignore deleted file mode 100644 index 1b8afd08..00000000 --- a/app/api/chat/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.ts \ No newline at end of file diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts deleted file mode 100644 index 18c7db14..00000000 --- a/app/api/chat/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { OpenAIApi, Configuration } from "openai"; -import { ChatRequest } from "./typing"; - -export async function POST(req: Request) { - try { - 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); - return new Response(JSON.stringify(e)); - } -} diff --git a/app/api/common.ts b/app/api/common.ts new file mode 100644 index 00000000..842eeaca --- /dev/null +++ b/app/api/common.ts @@ -0,0 +1,22 @@ +import { NextRequest } from "next/server"; + +const OPENAI_URL = "api.openai.com"; +const DEFAULT_PROTOCOL = "https"; +const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL; +const BASE_URL = process.env.BASE_URL ?? OPENAI_URL; + +export async function requestOpenai(req: NextRequest) { + const apiKey = req.headers.get("token"); + const openaiPath = req.headers.get("path"); + + console.log("[Proxy] ", openaiPath); + + return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + method: req.method, + body: req.body, + }); +} diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts new file mode 100644 index 00000000..5bc317e5 --- /dev/null +++ b/app/api/openai/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requestOpenai } from "../common"; + +async function makeRequest(req: NextRequest) { + try { + const res = await requestOpenai(req); + return new Response(res.body); + } catch (e) { + console.error("[OpenAI] ", req.body, e); + return NextResponse.json( + { + error: true, + msg: JSON.stringify(e), + }, + { + status: 500, + }, + ); + } +} + +export async function POST(req: NextRequest) { + return makeRequest(req); +} + +export async function GET(req: NextRequest) { + return makeRequest(req); +} diff --git a/app/api/chat/typing.ts b/app/api/openai/typing.ts similarity index 100% rename from app/api/chat/typing.ts rename to app/api/openai/typing.ts diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 241cebae..43d14fc4 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -27,6 +27,7 @@ import { getCurrentCommitId } from "../utils"; import Link from "next/link"; import { UPDATE_URL } from "../constant"; import { SearchService, usePromptStore } from "../store/prompt"; +import { requestUsage } from "../requests"; function SettingItem(props: { title: string; @@ -54,7 +55,7 @@ export function Settings(props: { closeSettings: () => void }) { state.updateConfig, state.resetConfig, state.clearAllData, - ] + ], ); const updateStore = useUpdateStore(); @@ -70,14 +71,34 @@ export function Settings(props: { closeSettings: () => void }) { }); } + const [usage, setUsage] = useState<{ + granted?: number; + used?: number; + }>(); + const [loadingUsage, setLoadingUsage] = useState(false); + function checkUsage() { + setLoadingUsage(true); + requestUsage() + .then((res) => + setUsage({ + granted: res?.total_granted, + used: res?.total_used, + }), + ) + .finally(() => { + setLoadingUsage(false); + }); + } + useEffect(() => { checkUpdate(); + checkUsage(); }, []); const accessStore = useAccessStore(); const enabledAccessControl = useMemo( () => accessStore.enabledAccessControl(), - [] + [], ); const promptStore = usePromptStore(); @@ -179,7 +200,7 @@ export function Settings(props: { closeSettings: () => void }) { onChange={(e) => { updateConfig( (config) => - (config.submitKey = e.target.value as any as SubmitKey) + (config.submitKey = e.target.value as any as SubmitKey), ); }} > @@ -199,7 +220,7 @@ export function Settings(props: { closeSettings: () => void }) { value={config.theme} onChange={(e) => { updateConfig( - (config) => (config.theme = e.target.value as any as Theme) + (config) => (config.theme = e.target.value as any as Theme), ); }} > @@ -240,7 +261,7 @@ export function Settings(props: { closeSettings: () => void }) { onChange={(e) => updateConfig( (config) => - (config.fontSize = Number.parseInt(e.currentTarget.value)) + (config.fontSize = Number.parseInt(e.currentTarget.value)), ) } > @@ -253,7 +274,7 @@ export function Settings(props: { closeSettings: () => void }) { checked={config.tightBorder} onChange={(e) => updateConfig( - (config) => (config.tightBorder = e.currentTarget.checked) + (config) => (config.tightBorder = e.currentTarget.checked), ) } > @@ -271,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) { onChange={(e) => updateConfig( (config) => - (config.disablePromptHint = e.currentTarget.checked) + (config.disablePromptHint = e.currentTarget.checked), ) } > @@ -281,7 +302,7 @@ export function Settings(props: { closeSettings: () => void }) { title={Locale.Settings.Prompt.List} subTitle={Locale.Settings.Prompt.ListCount( builtinCount, - customCount + customCount, )} > void }) { > + + {loadingUsage ? ( +
+ ) : ( + } + text={Locale.Settings.Usage.Check} + onClick={checkUsage} + /> + )} + + void }) { onChange={(e) => updateConfig( (config) => - (config.historyMessageCount = e.target.valueAsNumber) + (config.historyMessageCount = e.target.valueAsNumber), ) } > @@ -357,7 +400,7 @@ export function Settings(props: { closeSettings: () => void }) { updateConfig( (config) => (config.compressMessageLengthThreshold = - e.currentTarget.valueAsNumber) + e.currentTarget.valueAsNumber), ) } > @@ -370,7 +413,8 @@ export function Settings(props: { closeSettings: () => void }) { value={config.modelConfig.model} onChange={(e) => { updateConfig( - (config) => (config.modelConfig.model = e.currentTarget.value) + (config) => + (config.modelConfig.model = e.currentTarget.value), ); }} > @@ -395,7 +439,7 @@ export function Settings(props: { closeSettings: () => void }) { updateConfig( (config) => (config.modelConfig.temperature = - e.currentTarget.valueAsNumber) + e.currentTarget.valueAsNumber), ); }} > @@ -413,7 +457,7 @@ export function Settings(props: { closeSettings: () => void }) { updateConfig( (config) => (config.modelConfig.max_tokens = - e.currentTarget.valueAsNumber) + e.currentTarget.valueAsNumber), ) } > @@ -432,7 +476,7 @@ export function Settings(props: { closeSettings: () => void }) { updateConfig( (config) => (config.modelConfig.presence_penalty = - e.currentTarget.valueAsNumber) + e.currentTarget.valueAsNumber), ); }} > diff --git a/app/locales/cn.ts b/app/locales/cn.ts index e9cbad9e..239da23f 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -64,6 +64,7 @@ const cn = { Title: "字体大小", SubTitle: "聊天内容的字体大小", }, + Update: { Version: (x: string) => `当前版本:${x}`, IsLatest: "已是最新版本", @@ -98,6 +99,14 @@ const cn = { SubTitle: "使用自己的 Key 可绕过受控访问限制", Placeholder: "OpenAI API Key", }, + Usage: { + Title: "账户余额", + SubTitle(granted: any, used: any) { + return `总共 $${granted},已使用 $${used}`; + }, + IsChecking: "正在检查…", + Check: "重新检查", + }, AccessCode: { Title: "访问码", SubTitle: "现在是受控访问状态", diff --git a/app/locales/en.ts b/app/locales/en.ts index f31badd0..29699243 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -101,6 +101,14 @@ const en: LocaleType = { SubTitle: "Use your key to ignore access code limit", Placeholder: "OpenAI API Key", }, + Usage: { + Title: "Account Balance", + SubTitle(granted: any, used: any) { + return `Total $${granted}, Used $${used}`; + }, + IsChecking: "Checking...", + Check: "Check Again", + }, AccessCode: { Title: "Access Code", SubTitle: "Access control enabled", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index b78e1b83..e63c57a6 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -99,6 +99,14 @@ const tw: LocaleType = { SubTitle: "使用自己的 Key 可規避受控訪問限制", Placeholder: "OpenAI API Key", }, + Usage: { + Title: "帳戶餘額", + SubTitle(granted: any, used: any) { + return `總共 $${granted},已使用 $${used}`; + }, + IsChecking: "正在檢查…", + Check: "重新檢查", + }, AccessCode: { Title: "訪問碼", SubTitle: "現在是受控訪問狀態", diff --git a/app/requests.ts b/app/requests.ts index e9da8708..d173eb0d 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -1,4 +1,4 @@ -import type { ChatRequest, ChatReponse } from "./api/chat/typing"; +import type { ChatRequest, ChatReponse } from "./api/openai/typing"; import { filterConfig, Message, ModelConfig, useAccessStore } from "./store"; import Locale from "./locales"; @@ -9,7 +9,7 @@ const makeRequestParam = ( options?: { filterBot?: boolean; stream?: boolean; - } + }, ): ChatRequest => { let sendMessages = messages.map((v) => ({ role: v.role, @@ -42,19 +42,48 @@ function getHeaders() { return headers; } +export function requestOpenaiClient(path: string) { + return (body: any, method = "POST") => + fetch("/api/openai", { + method, + headers: { + "Content-Type": "application/json", + path, + ...getHeaders(), + }, + body: body && JSON.stringify(body), + }); +} + export async function requestChat(messages: Message[]) { const req: ChatRequest = makeRequestParam(messages, { filterBot: true }); - const res = await fetch("/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...getHeaders(), - }, - body: JSON.stringify(req), - }); + const res = await requestOpenaiClient("v1/chat/completions")(req); - return (await res.json()) as ChatReponse; + try { + const response = (await res.json()) as ChatReponse; + return response; + } catch (error) { + console.error("[Request Chat] ", error, res.body); + } +} + +export async function requestUsage() { + const res = await requestOpenaiClient("dashboard/billing/credit_grants")( + null, + "GET", + ); + + try { + const response = (await res.json()) as { + total_available: number; + total_granted: number; + total_used: number; + }; + return response; + } catch (error) { + console.error("[Request usage] ", error, res.body); + } } export async function requestChatStream( @@ -65,7 +94,7 @@ export async function requestChatStream( onMessage: (message: string, done: boolean) => void; onError: (error: Error) => void; onController?: (controller: AbortController) => void; - } + }, ) { const req = makeRequestParam(messages, { stream: true, @@ -87,6 +116,7 @@ export async function requestChatStream( method: "POST", headers: { "Content-Type": "application/json", + path: "v1/chat/completions", ...getHeaders(), }, body: JSON.stringify(req), @@ -129,7 +159,7 @@ export async function requestChatStream( responseText = Locale.Error.Unauthorized; finish(); } else { - console.error("Stream Error"); + console.error("Stream Error", res.body); options?.onError(new Error("Stream Error")); } } catch (err) { @@ -149,7 +179,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) { const res = await requestChat(messages); - return res.choices.at(0)?.message?.content ?? ""; + return res?.choices?.at(0)?.message?.content ?? ""; } // To store message streaming controller @@ -159,7 +189,7 @@ export const ControllerPool = { addController( sessionIndex: number, messageIndex: number, - controller: AbortController + controller: AbortController, ) { const key = this.key(sessionIndex, messageIndex); this.controllers[key] = controller; diff --git a/middleware.ts b/middleware.ts index 7e671ff1..12b49ac6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,7 +6,7 @@ export const config = { matcher: ["/api/chat", "/api/chat-stream"], }; -export function middleware(req: NextRequest, res: NextResponse) { +export function middleware(req: NextRequest) { const accessCode = req.headers.get("access-code"); const token = req.headers.get("token"); const hashedCode = md5.hash(accessCode ?? "").trim(); @@ -18,14 +18,33 @@ export function middleware(req: NextRequest, res: NextResponse) { if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) { return NextResponse.json( { + error: true, needAccessCode: true, - hint: "Please go settings page and fill your access code.", + msg: "Please go settings page and fill your access code.", }, { status: 401, - } + }, ); } + // inject api key + if (!token) { + const apiKey = process.env.OPENAI_API_KEY; + if (apiKey) { + req.headers.set("token", apiKey); + } else { + return NextResponse.json( + { + error: true, + msg: "Empty Api Key", + }, + { + status: 401, + }, + ); + } + } + return NextResponse.next(); } diff --git a/public/serviceWorker.js b/public/serviceWorker.js index 585633fc..028c79a8 100644 --- a/public/serviceWorker.js +++ b/public/serviceWorker.js @@ -1,24 +1,13 @@ const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; -self.addEventListener('activate', function (event) { - console.log('ServiceWorker activated.'); +self.addEventListener("activate", function (event) { + console.log("ServiceWorker activated."); }); -self.addEventListener('install', function (event) { +self.addEventListener("install", function (event) { event.waitUntil( - caches.open(CHATGPT_NEXT_WEB_CACHE) - .then(function (cache) { - return cache.addAll([ - ]); - }) + caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) { + return cache.addAll([]); + }), ); }); - -self.addEventListener('fetch', function (event) { - event.respondWith( - caches.match(event.request) - .then(function (response) { - return response || fetch(event.request); - }) - ); -}); \ No newline at end of file From cd9799588da9269b0e7cb55b2a37501d319b8e50 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 17:53:36 +0000 Subject: [PATCH 05/11] fixup --- middleware.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 12b49ac6..0988f54a 100644 --- a/middleware.ts +++ b/middleware.ts @@ -31,6 +31,7 @@ export function middleware(req: NextRequest) { // inject api key if (!token) { const apiKey = process.env.OPENAI_API_KEY; + console.log("apiKey", apiKey); if (apiKey) { req.headers.set("token", apiKey); } else { @@ -46,5 +47,9 @@ export function middleware(req: NextRequest) { } } - return NextResponse.next(); + return NextResponse.next({ + request: { + headers: req.headers, + }, + }); } From 469c8e9b0097224fd447cdbfa246cfc5022a82e7 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 17:53:46 +0000 Subject: [PATCH 06/11] fixup --- middleware.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 0988f54a..b49a7225 100644 --- a/middleware.ts +++ b/middleware.ts @@ -31,7 +31,6 @@ export function middleware(req: NextRequest) { // inject api key if (!token) { const apiKey = process.env.OPENAI_API_KEY; - console.log("apiKey", apiKey); if (apiKey) { req.headers.set("token", apiKey); } else { From 53e30e20db87f6e1a295e392c4a483b48b8246bd Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 29 Mar 2023 18:09:15 +0000 Subject: [PATCH 07/11] fix: middleware match error --- middleware.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index b49a7225..9338a2c6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -3,7 +3,7 @@ import { ACCESS_CODES } from "./app/api/access"; import md5 from "spark-md5"; export const config = { - matcher: ["/api/chat", "/api/chat-stream"], + matcher: ["/api/openai", "/api/chat-stream"], }; export function middleware(req: NextRequest) { @@ -32,6 +32,7 @@ export function middleware(req: NextRequest) { if (!token) { const apiKey = process.env.OPENAI_API_KEY; if (apiKey) { + console.log("[Auth] set system token"); req.headers.set("token", apiKey); } else { return NextResponse.json( @@ -44,6 +45,8 @@ export function middleware(req: NextRequest) { }, ); } + } else { + console.log("[Auth] set user token"); } return NextResponse.next({ From 8b4db412d801ed1edea122da4dcfb9788de31417 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 30 Mar 2023 02:30:38 +0800 Subject: [PATCH 08/11] Update home.tsx --- app/components/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index 847b6cf4..a44e5f16 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -454,7 +454,7 @@ export function Chat(props: {
); })} -
+
-
From d9fc9cd198e8e090907c7109bd31326eae85ef3e Mon Sep 17 00:00:00 2001 From: angular-moon Date: Thu, 30 Mar 2023 10:16:00 +0800 Subject: [PATCH 09/11] onUserSubmit hide promptHints --- app/components/home.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index a44e5f16..10f58ebe 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -102,7 +102,7 @@ export function ChatList() { state.currentSessionIndex, state.selectSession, state.removeSession, - ], + ] ); return ( @@ -196,7 +196,7 @@ export function Chat(props: { setPromptHints(promptStore.search(text)); }, 100, - { leading: true, trailing: true }, + { leading: true, trailing: true } ); const onPromptSelect = (prompt: Prompt) => { @@ -210,7 +210,7 @@ export function Chat(props: { if (!dom) return; const paddingBottomNum: number = parseInt( window.getComputedStyle(dom).paddingBottom, - 10, + 10 ); dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; }; @@ -239,6 +239,7 @@ export function Chat(props: { setIsLoading(true); chatStore.onUserInput(userInput).then(() => setIsLoading(false)); setUserInput(""); + setPromptHints([]); inputRef.current?.focus(); }; @@ -299,7 +300,7 @@ export function Chat(props: { preview: true, }, ] - : [], + : [] ) .concat( userInput.length > 0 @@ -311,7 +312,7 @@ export function Chat(props: { preview: true, }, ] - : [], + : [] ); // auto scroll @@ -339,7 +340,7 @@ export function Chat(props: { const newTopic = prompt(Locale.Chat.Rename, session.topic); if (newTopic && newTopic !== session.topic) { chatStore.updateCurrentSession( - (session) => (session.topic = newTopic!), + (session) => (session.topic = newTopic!) ); } }} @@ -584,7 +585,7 @@ export function Home() { state.newSession, state.currentSessionIndex, state.removeSession, - ], + ] ); const loading = !useHasHydrated(); const [showSideBar, setShowSideBar] = useState(true); From d5235c81d0bf4c15e173eabdafc846d56f1a75ed Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 30 Mar 2023 11:21:44 +0800 Subject: [PATCH 10/11] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 022fada7..bebb6378 100644 --- a/README.md +++ b/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) / [加入 Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) +[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donate](#捐赠-donate-usdt) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) @@ -179,6 +179,12 @@ The free trial of the OpenAI account used by the demo will expire on April 1, 20 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. +## 捐赠 Donate USDT +> BNB Smart Chain (BEP 20) +``` +0x67cD02c7EB62641De576a1fA3EdB32eA0c3ffD89 +``` + ## 鸣谢 Special Thanks ### 捐赠者 Sponsor From 8d6d6bbf5ded956dd567e71212ce44a6a73de290 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Thu, 30 Mar 2023 11:22:27 +0800 Subject: [PATCH 11/11] Update README.md --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index bebb6378..5952aa33 100644 --- a/README.md +++ b/README.md @@ -169,15 +169,6 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next- ![更多展示 More](./static/more.png) -## 说明 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. ## 捐赠 Donate USDT > BNB Smart Chain (BEP 20)