diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 52feae50e..15d324074 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -31,3 +31,10 @@ jobs:
# Set test_mode true to run tests instead of the true action!!
test_mode: false
+
+ - name: Sync check
+ if: failure()
+ run: |
+ echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
+ echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
+ exit 1
diff --git a/README.md b/README.md
index 0607950c2..a6288798a 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
-[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231095592-330adc52-0337-4c13-8452-938ec169e367.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
+[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231789746-41f34d05-6ef9-43f3-a1d1-ff109d4c3c14.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&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
@@ -97,6 +97,7 @@ We recommend that you follow the steps below to re-deploy:
- Choose and deploy in Vercel again, [please see the detailed tutorial](./docs/vercel-cn.md).
### Enable Automatic Updates
+
After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:

@@ -104,6 +105,7 @@ After forking the project, due to the limitations imposed by Github, you need to

### Manually Updating Code
+
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
You can star or watch this project or follow author to get release notifictions in time.
@@ -134,18 +136,12 @@ Access passsword, separated by comma.
### `BASE_URL` (optional)
-> Default: `api.openai.com`
+> Default: `https://api.openai.com`
+
+> Examples: `http://your-openai-proxy.com`
Override openai api request base url.
-### `PROTOCOL` (optional)
-
-> Default: `https`
-
-> Values: `http` | `https`
-
-Override openai api request protocol.
-
## Development
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
@@ -213,6 +209,8 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
### Sponsor
+> 仅列出捐赠金额 >= 100RMB 的用户。
+
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
@@ -222,6 +220,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
[@webees](https://github.com/webees)
[@chazzhou](https://github.com/chazzhou)
[@hauy](https://github.com/hauy)
+[@Corwin006](https://github.com/Corwin006)
### Contributor
diff --git a/README_CN.md b/README_CN.md
index db08ee445..d2d64aa00 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -43,6 +43,7 @@
- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
### 打开自动更新
+
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:

@@ -85,17 +86,13 @@ OpanAI 密钥,你在 openai 账户页面申请的 api key。
### `BASE_URL` (可选)
-> Default: `api.openai.com`
+> Default: `https://api.openai.com`
+
+> Examples: `http://your-openai-proxy.com`
OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填写此选项。
-### `PROTOCOL` (可选)
-
-> Default: `https`
-
-> Values: `http` | `https`
-
-OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此选项设置为 http。
+> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
## 开发
@@ -137,7 +134,8 @@ docker run -d -p 3000:3000 \
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
- -e PROXY_URL="http://localhost:7890" \
+ --net=host \
+ -e PROXY_URL="http://127.0.0.1:7890" \
yidadaa/chatgpt-next-web
```
@@ -153,12 +151,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
### 捐赠者
-> 仅列出了部分大额打赏,小额打赏(< 100RMB)人数太多,在此不再列出,敬请谅解。
-
-[@mushan0x0](https://github.com/mushan0x0)
-[@ClarenceDan](https://github.com/ClarenceDan)
-[@zhangjia](https://github.com/zhangjia)
-[@hoochanlon](https://github.com/hoochanlon)
+> 见英文版。
### 贡献者
diff --git a/app/api/common.ts b/app/api/common.ts
index 842eeacaf..53ab18ed6 100644
--- a/app/api/common.ts
+++ b/app/api/common.ts
@@ -9,9 +9,16 @@ export async function requestOpenai(req: NextRequest) {
const apiKey = req.headers.get("token");
const openaiPath = req.headers.get("path");
- console.log("[Proxy] ", openaiPath);
+ let baseUrl = BASE_URL;
- return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
+ if (!baseUrl.startsWith("http")) {
+ baseUrl = `${PROTOCOL}://${baseUrl}`;
+ }
+
+ console.log("[Proxy] ", openaiPath);
+ console.log("[Base Url]", baseUrl);
+
+ return fetch(`${baseUrl}/${openaiPath}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
index f57e6c100..7cd2889f7 100644
--- a/app/components/chat.module.scss
+++ b/app/components/chat.module.scss
@@ -1,5 +1,29 @@
@import "../styles/animation.scss";
+.chat-input-actions {
+ display: flex;
+ flex-wrap: wrap;
+
+ .chat-input-action {
+ display: inline-flex;
+ border-radius: 20px;
+ font-size: 12px;
+ background-color: var(--white);
+ color: var(--black);
+ border: var(--border-in-light);
+ padding: 4px 10px;
+ animation: slide-in ease 0.3s;
+ box-shadow: var(--card-shadow);
+ transition: all ease 0.3s;
+ margin-bottom: 10px;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+}
+
.prompt-toast {
position: absolute;
bottom: -50px;
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 33ac3ac57..f7b67b293 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -3,6 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
+import RenameIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
@@ -14,6 +15,11 @@ import DeleteIcon from "../icons/delete.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
+import LightIcon from "../icons/light.svg";
+import DarkIcon from "../icons/dark.svg";
+import AutoIcon from "../icons/auto.svg";
+import BottomIcon from "../icons/bottom.svg";
+
import {
Message,
SubmitKey,
@@ -22,6 +28,7 @@ import {
ROLES,
createMessage,
useAccessStore,
+ Theme,
} from "../store";
import {
@@ -31,6 +38,7 @@ import {
isMobileScreen,
selectOrCopy,
autoGrowTextArea,
+ getCSSVar,
} from "../utils";
import dynamic from "next/dynamic";
@@ -60,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) {
const config = useChatStore((state) => state.config);
if (props.role !== "user") {
- return ;
+ return (
+
+
+
+ );
}
return (
@@ -316,22 +328,78 @@ function useScrollToBottom() {
// for auto-scroll
const scrollRef = useRef(null);
const [autoScroll, setAutoScroll] = useState(true);
+ const scrollToBottom = () => {
+ const dom = scrollRef.current;
+ if (dom) {
+ setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
+ }
+ };
// auto scroll
useLayoutEffect(() => {
- const dom = scrollRef.current;
- if (dom && autoScroll) {
- setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
- }
+ autoScroll && scrollToBottom();
});
return {
scrollRef,
autoScroll,
setAutoScroll,
+ scrollToBottom,
};
}
+export function ChatActions(props: {
+ showPromptModal: () => void;
+ scrollToBottom: () => void;
+ hitBottom: boolean;
+}) {
+ const chatStore = useChatStore();
+
+ const theme = chatStore.config.theme;
+
+ function nextTheme() {
+ const themes = [Theme.Auto, Theme.Light, Theme.Dark];
+ const themeIndex = themes.indexOf(theme);
+ const nextIndex = (themeIndex + 1) % themes.length;
+ const nextTheme = themes[nextIndex];
+ chatStore.updateConfig((config) => (config.theme = nextTheme));
+ }
+
+ return (
+
+ {!props.hitBottom && (
+
+
+
+ )}
+ {props.hitBottom && (
+
+
+
+ )}
+
+
+ {theme === Theme.Auto ? (
+
+ ) : theme === Theme.Light ? (
+
+ ) : theme === Theme.Dark ? (
+
+ ) : null}
+
+
+ );
+}
+
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
@@ -350,7 +418,7 @@ export function Chat(props: {
const [beforeInput, setBeforeInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
- const { scrollRef, setAutoScroll } = useScrollToBottom();
+ const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(false);
const onChatBodyScroll = (e: HTMLElement) => {
@@ -375,16 +443,6 @@ export function Chat(props: {
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;
- };
-
// auto grow input
const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback(
@@ -409,7 +467,6 @@ export function Chat(props: {
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => {
- scrollInput();
setUserInput(text);
const n = text.trim().length;
@@ -533,6 +590,13 @@ export function Chat(props: {
const [showPromptModal, setShowPromptModal] = useState(false);
+ const renameSession = () => {
+ const newTopic = prompt(Locale.Chat.Rename, session.topic);
+ if (newTopic && newTopic !== session.topic) {
+ chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
+ }
+ };
+
// Auto focus
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
@@ -546,14 +610,7 @@ export function Chat(props: {
{
- const newTopic = prompt(Locale.Chat.Rename, session.topic);
- if (newTopic && newTopic !== session.topic) {
- chatStore.updateCurrentSession(
- (session) => (session.topic = newTopic!),
- );
- }
- }}
+ onClickCapture={renameSession}
>
{session.topic}
@@ -572,12 +629,9 @@ export function Chat(props: {
}
+ icon={}
bordered
- title={Locale.Chat.Actions.CompressedHistory}
- onClick={() => {
- setShowPromptModal(true);
- }}
+ onClick={renameSession}
/>
@@ -672,22 +726,20 @@ export function Chat(props: {
)}
- {(message.preview || message.content.length === 0) &&
- !isUser ? (
-
- ) : (
- onRightClick(e, message)}
- onDoubleClickCapture={() => {
- if (!isMobileScreen()) return;
- setUserInput(message.content);
- }}
- >
-
-
- )}
+ onRightClick(e, message)}
+ onDoubleClickCapture={() => {
+ if (!isMobileScreen()) return;
+ setUserInput(message.content);
+ }}
+ fontSize={fontSize}
+ parentRef={scrollRef}
+ />
{!isUser && !message.preview && (
@@ -704,6 +756,12 @@ export function Chat(props: {
+
+
setShowPromptModal(true)}
+ scrollToBottom={scrollToBottom}
+ hitBottom={hitBottom}
+ />
);
}
diff --git a/app/global.d.ts b/app/global.d.ts
new file mode 100644
index 000000000..bd1c062de
--- /dev/null
+++ b/app/global.d.ts
@@ -0,0 +1,11 @@
+declare module "*.jpg";
+declare module "*.png";
+declare module "*.woff2";
+declare module "*.woff";
+declare module "*.ttf";
+declare module "*.scss" {
+ const content: Record;
+ export default content;
+}
+
+declare module "*.svg";
diff --git a/app/icons/auto.svg b/app/icons/auto.svg
new file mode 100644
index 000000000..6745dfbd0
--- /dev/null
+++ b/app/icons/auto.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/bottom.svg b/app/icons/bottom.svg
new file mode 100644
index 000000000..06c663ab2
--- /dev/null
+++ b/app/icons/bottom.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/dark.svg b/app/icons/dark.svg
new file mode 100644
index 000000000..3eebc373e
--- /dev/null
+++ b/app/icons/dark.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/light.svg b/app/icons/light.svg
new file mode 100644
index 000000000..22cfa1fff
--- /dev/null
+++ b/app/icons/light.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/rename.svg b/app/icons/rename.svg
new file mode 100644
index 000000000..cee69eb8d
--- /dev/null
+++ b/app/icons/rename.svg
@@ -0,0 +1 @@
+
diff --git a/app/locales/it.ts b/app/locales/it.ts
index 3cd768fed..c785d616e 100644
--- a/app/locales/it.ts
+++ b/app/locales/it.ts
@@ -45,12 +45,12 @@ const it: LocaleType = {
Send: "Send Memory",
Reset: "Reset Session",
ResetConfirm:
- "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
+ "Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
- DeleteToast: "Chat Deleted",
+ DeleteToast: "Chat Cancellata",
Revert: "Revert",
},
Settings: {
@@ -93,9 +93,9 @@ const it: LocaleType = {
GoToUpdate: "Aggiorna",
},
SendKey: "Tasto invia",
- Theme: "tema",
- TightBorder: "Bordi stretti",
- SendPreviewBubble: "Invia l'anteprima della bolla",
+ Theme: "Tema",
+ TightBorder: "Schermo intero",
+ SendPreviewBubble: "Anteprima di digitazione",
Prompt: {
Disable: {
Title: "Disabilita l'auto completamento",
@@ -116,7 +116,7 @@ const it: LocaleType = {
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
- Title: "Chiave API",
+ Title: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
@@ -124,7 +124,7 @@ const it: LocaleType = {
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
- return `Usato in questo mese $${used}, subscription $${total}`;
+ return `Attualmente usato in questo mese $${used}, soglia massima $${total}`;
},
IsChecking: "Controllando...",
Check: "Controlla ancora",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index 77975b896..b239ed65d 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -152,9 +152,9 @@ const tw: LocaleType = {
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
- Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
+ Topic: "Summarise the conversation in a short and concise eye-catching title that instantly conveys the main topic. Use as few words as possible. Use the language used in the enquiry, e.g. use English for English enquiry, use zh-hant for traditional chinese enquiry. Don't use quotation marks at the beginning and the end.",
Summarize:
- "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
+ "Summarise the conversation in at most 250 tokens for continuing the conversation in future. Use the language used in the conversation, e.g. use English for English conversation, use zh-hant for traditional chinese conversation.",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
diff --git a/app/requests.ts b/app/requests.ts
index 86d180f71..c48ef2817 100644
--- a/app/requests.ts
+++ b/app/requests.ts
@@ -189,8 +189,8 @@ export async function requestChatStream(
finish();
} else if (res.status === 401) {
- console.error("Anauthorized");
- options?.onError(new Error("Anauthorized"), res.status);
+ console.error("Unauthorized");
+ options?.onError(new Error("Unauthorized"), res.status);
} else {
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"), res.status);
diff --git a/app/styles/globals.scss b/app/styles/globals.scss
index 53902d935..cf36ee92b 100644
--- a/app/styles/globals.scss
+++ b/app/styles/globals.scss
@@ -1,4 +1,6 @@
@mixin light {
+ --theme: light;
+
/* color */
--white: white;
--black: rgb(48, 48, 48);
@@ -18,6 +20,8 @@
}
@mixin dark {
+ --theme: dark;
+
/* color */
--white: rgb(30, 30, 30);
--black: rgb(187, 187, 187);
@@ -31,6 +35,10 @@
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
+
+ div:not(.no-dark) > svg {
+ filter: invert(0.5);
+ }
}
.light {
@@ -282,10 +290,6 @@ pre {
.clickable {
cursor: pointer;
- div:not(.no-dark) > svg {
- filter: invert(0.5);
- }
-
&:hover {
filter: brightness(0.9);
}
diff --git a/app/utils.ts b/app/utils.ts
index 5c2b06975..0e4a8eaea 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -120,3 +120,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
return rows;
}
+
+export function getCSSVar(varName: string) {
+ return getComputedStyle(document.body).getPropertyValue(varName).trim();
+}