resolve conflict
This commit is contained in:
commit
4dd2ef71fa
|
@ -37,20 +37,22 @@ One-Click to deploy your own ChatGPT web UI.
|
||||||
- One-click export all chat history with full Markdown support
|
- One-click export all chat history with full Markdown support
|
||||||
|
|
||||||
## 开发计划 Roadmap
|
## 开发计划 Roadmap
|
||||||
|
|
||||||
- System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
- System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||||
- User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
|
- User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
|
||||||
- Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
|
- Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
|
||||||
- Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
- Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
|
||||||
### 不会开发的功能 Not in Plan
|
### 不会开发的功能 Not in Plan
|
||||||
- User login, accounts, cloud sync 用户登陆、账号管理、消息云同步
|
|
||||||
|
- User login, accounts, cloud sync 用户登录、账号管理、消息云同步
|
||||||
- UI text customize 界面文字自定义
|
- UI text customize 界面文字自定义
|
||||||
|
|
||||||
## 开始使用
|
## 开始使用
|
||||||
|
|
||||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
||||||
2. 点击右侧按钮开始部署:
|
2. 点击右侧按钮开始部署:
|
||||||
[](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),直接使用 Github 账号登陆即可,记得在环境变量页填入 API Key;
|
[](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),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key;
|
||||||
3. 部署完毕后,即可开始使用;
|
3. 部署完毕后,即可开始使用;
|
||||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
||||||
|
|
||||||
|
@ -179,9 +181,10 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 捐赠 Donate USDT
|
## 捐赠 Donate USDT
|
||||||
|
|
||||||
> BNB Smart Chain (BEP 20)
|
> BNB Smart Chain (BEP 20)
|
||||||
|
|
||||||
```
|
```
|
||||||
0x67cD02c7EB62641De576a1fA3EdB32eA0c3ffD89
|
0x67cD02c7EB62641De576a1fA3EdB32eA0c3ffD89
|
||||||
```
|
```
|
||||||
|
|
|
@ -3,8 +3,10 @@ import { requestOpenai } from "../common";
|
||||||
|
|
||||||
async function makeRequest(req: NextRequest) {
|
async function makeRequest(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const res = await requestOpenai(req);
|
const api = await requestOpenai(req);
|
||||||
return new Response(res.body);
|
const res = new NextResponse(api.body);
|
||||||
|
res.headers.set("Content-Type", "application/json");
|
||||||
|
return res;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[OpenAI] ", req.body, e);
|
console.error("[OpenAI] ", req.body, e);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|
|
@ -72,7 +72,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile {
|
.mobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,9 +170,9 @@ function useSubmitHandler() {
|
||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
const submitKey = config.submitKey;
|
const submitKey = config.submitKey;
|
||||||
|
|
||||||
const shouldSubmit = (e: KeyboardEvent) => {
|
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key !== "Enter") return false;
|
if (e.key !== "Enter") return false;
|
||||||
|
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
|
||||||
return (
|
return (
|
||||||
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
||||||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
||||||
|
@ -294,7 +294,7 @@ export function Chat() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// check if should send message
|
// check if should send message
|
||||||
const onInputKeyDown = (e: KeyboardEvent) => {
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (shouldSubmit(e)) {
|
if (shouldSubmit(e)) {
|
||||||
onUserSubmit();
|
onUserSubmit();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -330,6 +330,8 @@ export function Chat() {
|
||||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
const latestMessageRef = useRef<HTMLDivElement>(null);
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
// preview messages
|
// preview messages
|
||||||
const messages = (session.messages as RenderMessage[])
|
const messages = (session.messages as RenderMessage[])
|
||||||
.concat(
|
.concat(
|
||||||
|
@ -345,13 +347,13 @@ export function Chat() {
|
||||||
: [],
|
: [],
|
||||||
)
|
)
|
||||||
.concat(
|
.concat(
|
||||||
userInput.length > 0
|
userInput.length > 0 && config.sendPreviewBubble
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: userInput,
|
content: userInput,
|
||||||
date: new Date().toLocaleString(),
|
date: new Date().toLocaleString(),
|
||||||
preview: true,
|
preview: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
|
@ -382,14 +384,7 @@ export function Chat() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat} key={session.id}>
|
<div className={styles.chat} key={session.id}>
|
||||||
<div className={styles["window-header"]}>
|
<div className={styles["window-header"]}>
|
||||||
<div
|
<div className={styles["window-header-title"]}>
|
||||||
className={styles["window-header-title"]}
|
|
||||||
//onClick={() => {
|
|
||||||
// if (window.innerWidth < 768) {
|
|
||||||
// setSideBarCollapse(!sidebarCollapse);
|
|
||||||
// }
|
|
||||||
//}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -532,7 +527,7 @@ export function Chat() {
|
||||||
rows={4}
|
rows={4}
|
||||||
onInput={(e) => onInput(e.currentTarget.value)}
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
value={userInput}
|
value={userInput}
|
||||||
onKeyDown={(e) => onInputKeyDown(e as any)}
|
onKeyDown={onInputKeyDown}
|
||||||
onFocus={() => setAutoScroll(true)}
|
onFocus={() => setAutoScroll(true)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
|
|
|
@ -49,14 +49,14 @@ function SettingItem(props: {
|
||||||
|
|
||||||
export function Settings(props: { closeSettings: () => void }) {
|
export function Settings(props: { closeSettings: () => void }) {
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [config, updateConfig, resetConfig, clearAllData] = useChatStore(
|
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
|
||||||
(state) => [
|
useChatStore((state) => [
|
||||||
state.config,
|
state.config,
|
||||||
state.updateConfig,
|
state.updateConfig,
|
||||||
state.resetConfig,
|
state.resetConfig,
|
||||||
state.clearAllData,
|
state.clearAllData,
|
||||||
],
|
state.clearSessions,
|
||||||
);
|
]);
|
||||||
|
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||||
|
@ -120,7 +120,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ClearIcon />}
|
icon={<ClearIcon />}
|
||||||
onClick={clearAllData}
|
onClick={clearSessions}
|
||||||
bordered
|
bordered
|
||||||
title={Locale.Settings.Actions.ClearAll}
|
title={Locale.Settings.Actions.ClearAll}
|
||||||
/>
|
/>
|
||||||
|
@ -278,6 +278,19 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem title={Locale.Settings.SendPreviewBubble}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.sendPreviewBubble}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateConfig(
|
||||||
|
(config) =>
|
||||||
|
(config.sendPreviewBubble = e.currentTarget.checked),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</SettingItem>
|
||||||
</List>
|
</List>
|
||||||
<List>
|
<List>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
|
|
|
@ -77,6 +77,7 @@ const cn = {
|
||||||
SendKey: "发送键",
|
SendKey: "发送键",
|
||||||
Theme: "主题",
|
Theme: "主题",
|
||||||
TightBorder: "紧凑边框",
|
TightBorder: "紧凑边框",
|
||||||
|
SendPreviewBubble: "发送预览气泡",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "禁用提示词自动补全",
|
Title: "禁用提示词自动补全",
|
||||||
|
|
|
@ -78,6 +78,7 @@ const en: LocaleType = {
|
||||||
SendKey: "Send Key",
|
SendKey: "Send Key",
|
||||||
Theme: "Theme",
|
Theme: "Theme",
|
||||||
TightBorder: "Tight Border",
|
TightBorder: "Tight Border",
|
||||||
|
SendPreviewBubble: "Send Preview Bubble",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "Disable auto-completion",
|
Title: "Disable auto-completion",
|
||||||
|
|
|
@ -78,6 +78,7 @@ const es: LocaleType = {
|
||||||
SendKey: "Tecla de envío",
|
SendKey: "Tecla de envío",
|
||||||
Theme: "Tema",
|
Theme: "Tema",
|
||||||
TightBorder: "Borde ajustado",
|
TightBorder: "Borde ajustado",
|
||||||
|
SendPreviewBubble: "Send preview bubble",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "Desactivar autocompletado",
|
Title: "Desactivar autocompletado",
|
||||||
|
|
|
@ -77,6 +77,7 @@ const tw: LocaleType = {
|
||||||
SendKey: "發送鍵",
|
SendKey: "發送鍵",
|
||||||
Theme: "主題",
|
Theme: "主題",
|
||||||
TightBorder: "緊湊邊框",
|
TightBorder: "緊湊邊框",
|
||||||
|
SendPreviewBubble: "發送預覽氣泡",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "停用提示詞自動補全",
|
Title: "停用提示詞自動補全",
|
||||||
|
|
|
@ -2,6 +2,10 @@ import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
||||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
|
||||||
|
if (!Array.prototype.at) {
|
||||||
|
require("array.prototype.at/auto");
|
||||||
|
}
|
||||||
|
|
||||||
const TIME_OUT_MS = 30000;
|
const TIME_OUT_MS = 30000;
|
||||||
|
|
||||||
const makeRequestParam = (
|
const makeRequestParam = (
|
||||||
|
|
|
@ -11,6 +11,10 @@ import { trimTopic } from "../utils";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
|
if (!Array.prototype.at) {
|
||||||
|
require("array.prototype.at/auto");
|
||||||
|
}
|
||||||
|
|
||||||
export type Message = ChatCompletionResponseMessage & {
|
export type Message = ChatCompletionResponseMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
|
@ -39,6 +43,7 @@ export interface ChatConfig {
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
tightBorder: boolean;
|
tightBorder: boolean;
|
||||||
|
sendPreviewBubble: boolean;
|
||||||
|
|
||||||
disablePromptHint: boolean;
|
disablePromptHint: boolean;
|
||||||
|
|
||||||
|
@ -128,6 +133,7 @@ const DEFAULT_CONFIG: ChatConfig = {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
theme: Theme.Auto as Theme,
|
theme: Theme.Auto as Theme,
|
||||||
tightBorder: false,
|
tightBorder: false,
|
||||||
|
sendPreviewBubble: true,
|
||||||
|
|
||||||
disablePromptHint: false,
|
disablePromptHint: false,
|
||||||
|
|
||||||
|
@ -185,6 +191,7 @@ interface ChatStore {
|
||||||
config: ChatConfig;
|
config: ChatConfig;
|
||||||
sessions: ChatSession[];
|
sessions: ChatSession[];
|
||||||
currentSessionIndex: number;
|
currentSessionIndex: number;
|
||||||
|
clearSessions: () => void;
|
||||||
removeSession: (index: number) => void;
|
removeSession: (index: number) => void;
|
||||||
selectSession: (index: number) => void;
|
selectSession: (index: number) => void;
|
||||||
newSession: () => void;
|
newSession: () => void;
|
||||||
|
@ -225,6 +232,12 @@ export const useChatStore = create<ChatStore>()(
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearSessions() {
|
||||||
|
set(() => ({
|
||||||
|
sessions: [createEmptySession()],
|
||||||
|
currentSessionIndex: 0,
|
||||||
|
}));
|
||||||
|
},
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
set(() => ({ config: { ...DEFAULT_CONFIG } }));
|
set(() => ({ config: { ...DEFAULT_CONFIG } }));
|
||||||
},
|
},
|
||||||
|
@ -374,7 +387,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
const config = get().config;
|
const config = get().config;
|
||||||
const n = session.messages.length;
|
const n = session.messages.length;
|
||||||
const recentMessages = session.messages.slice(
|
const recentMessages = session.messages.slice(
|
||||||
n - config.historyMessageCount,
|
Math.max(0, n - config.historyMessageCount),
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoryPrompt = get().getMemoryPrompt();
|
const memoryPrompt = get().getMemoryPrompt();
|
||||||
|
|
|
@ -99,19 +99,19 @@ export const usePromptStore = create<PromptStore>()(
|
||||||
({
|
({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
} as Prompt)
|
} as Prompt),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.concat([...(state?.prompts?.values() ?? [])]);
|
.concat([...(state?.prompts?.values() ?? [])]);
|
||||||
|
|
||||||
const allPromptsForSearch = builtinPrompts.reduce(
|
const allPromptsForSearch = builtinPrompts.reduce(
|
||||||
(pre, cur) => pre.concat(cur),
|
(pre, cur) => pre.concat(cur),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||||
SearchService.init(allPromptsForSearch);
|
SearchService.init(allPromptsForSearch);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -51,10 +51,15 @@
|
||||||
--window-width: 90vw;
|
--window-width: 90vw;
|
||||||
--window-height: 90vh;
|
--window-height: 90vh;
|
||||||
--sidebar-width: 300px;
|
--sidebar-width: 300px;
|
||||||
|
<<<<<<< HEAD
|
||||||
--sidebar-collapse-width: 75px;
|
--sidebar-collapse-width: 75px;
|
||||||
--window-content-width: calc(100% - var(--sidebar-width));
|
--window-content-width: calc(100% - var(--sidebar-width));
|
||||||
--window-content-width-collapse: calc(100% - var(--sidebar-collapse-width));
|
--window-content-width-collapse: calc(100% - var(--sidebar-collapse-width));
|
||||||
--message-max-width: 90%;
|
--message-max-width: 90%;
|
||||||
|
=======
|
||||||
|
--window-content-width: calc(100% - var(--sidebar-width));
|
||||||
|
--message-max-width: 80%;
|
||||||
|
>>>>>>> a6c598c0172ee0121b92e7a114c7145846614006
|
||||||
--full-height: 100%;
|
--full-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
app/utils.ts
10
app/utils.ts
|
@ -2,15 +2,7 @@ import { showToast } from "./components/ui-lib";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
|
||||||
export function trimTopic(topic: string) {
|
export function trimTopic(topic: string) {
|
||||||
const s = topic.split("");
|
return topic.replace(/[,。!?、,.!?]*$/, "");
|
||||||
let lastChar = s.at(-1); // 获取 s 的最后一个字符
|
|
||||||
let pattern = /[,。!?、,.!?]/; // 定义匹配中文和英文标点符号的正则表达式
|
|
||||||
while (lastChar && pattern.test(lastChar!)) {
|
|
||||||
s.pop();
|
|
||||||
lastChar = s.at(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyToClipboard(text: string) {
|
export function copyToClipboard(text: string) {
|
||||||
|
|
|
@ -23,9 +23,9 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
"remark-breaks": "^3.0.2",
|
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"rehype-prism-plus": "^1.5.1",
|
"rehype-prism-plus": "^1.5.1",
|
||||||
|
"remark-breaks": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"sass": "^1.59.2",
|
"sass": "^1.59.2",
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.2",
|
"@types/spark-md5": "^3.0.2",
|
||||||
|
"array.prototype.at": "^1.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-next": "13.2.3",
|
"eslint-config-next": "13.2.3",
|
||||||
|
|
Loading…
Reference in New Issue