Compare commits

..

68 Commits
v1.5 ... v1.7

Author SHA1 Message Date
Yifei Zhang
8b4db412d8 Update home.tsx 2023-03-30 02:30:38 +08:00
Yifei Zhang
9c6f3ebb54 Merge pull request #179 from Yidadaa/prompt
fix: middleware match error
2023-03-30 02:11:11 +08:00
Yifei Zhang
53e30e20db fix: middleware match error 2023-03-29 18:09:15 +00:00
Yifei Zhang
d908099798 Merge pull request #172 from Yidadaa/prompt
feat: improve prompts ux
2023-03-30 02:01:55 +08:00
Yifei Zhang
469c8e9b00 fixup 2023-03-29 17:53:46 +00:00
Yifei Zhang
cd9799588d fixup 2023-03-29 17:53:36 +00:00
Yifei Zhang
447dec9444 feat: close #2 add check account balance 2023-03-29 17:45:26 +00:00
Yifei Zhang
45088a3e06 feat: #112 add edit chat title 2023-03-29 16:02:50 +00:00
Yifei Zhang
08f3c7026d feat: #170 auto scroll after retrying 2023-03-29 15:40:37 +00:00
Yifei Zhang
e606810581 refactor: #121 trigger auto-cmp with / prefix 2023-03-29 15:31:55 +00:00
Yifei Zhang
73b2ede53a Merge pull request #162 from HimiCos/main
fix: dialog height
2023-03-29 22:12:05 +08:00
Yifei Zhang
f1b2f895b4 Merge pull request #163 from GOWxx/bugfix_send_button_cover_text
fix: send button covering the text in the textarea
2023-03-29 22:11:34 +08:00
Yifei Zhang
9724308008 Merge pull request #164 from gtang8/patch-2
Update README.md
2023-03-29 22:05:22 +08:00
gtang8
be4706d02d Update README.md 2023-03-29 21:18:31 +08:00
HimiCos
752c083905 fix: dialog height 2023-03-29 20:53:23 +08:00
GOWxx
c47e90004a fix: send button covering the text in the textarea 2023-03-29 20:37:21 +08:00
Yifei Zhang
380f818285 fix: #159 temperature should range 0 - 2 2023-03-29 20:00:23 +08:00
Yifei Zhang
f893f53b1c Merge pull request #158 from HimiCos/main
chore: standardize input box punctuation
2023-03-29 19:45:22 +08:00
HimiCos
cac604aee3 chore: standardize input box punctuation 2023-03-29 19:31:34 +08:00
Yifei Zhang
525b5b8ee6 Merge pull request #148 from RugerMcCarthy/dev
supports the display of line breaks in Markdown
2023-03-29 18:05:52 +08:00
RugerMc
974c455bf9 feat: display line break hints in enter mode 2023-03-29 17:53:52 +08:00
RugerMc
b94607f636 feat: supports the display of line breaks in Markdown 2023-03-29 17:19:30 +08:00
Yifei Zhang
8a05f84838 Merge pull request #147 from Yidadaa/Yidadaa-patch-1
fix: #140 crash while hydrating store from old version
2023-03-29 16:09:06 +08:00
Yifei Zhang
ca0082856a Update settings.tsx 2023-03-29 16:06:22 +08:00
Yifei Zhang
e12238ba8b Merge pull request #146 from iSource/fix-mobile-autofocus
fix: mobile textarea autofocus ui error
2023-03-29 16:00:21 +08:00
iSource
71f119c9e8 fix: mobile textarea autofocus ui error 2023-03-29 15:45:31 +08:00
Yifei Zhang
067121d968 Merge pull request #89 from iFwu/main
feat: add font size config
2023-03-29 14:49:05 +08:00
Yifei Zhang
f5d775c055 Merge pull request #127 from stonega/main
fix: message top action style issue
2023-03-29 14:48:20 +08:00
伏晓
f979822508 feat: add font size setting 2023-03-29 13:15:32 +08:00
Yifei Zhang
cfbe6d77b5 Merge pull request #129 from HimiCos/main
Update app/locales/tw.ts
2023-03-29 12:36:30 +08:00
HimiCos
dee7950601 Update app/locales/tw.ts 2023-03-29 12:27:32 +08:00
Yifei Zhang
18d8eb4767 Merge pull request #125 from basefas/main
chore: move some dependencies to devDependencies
2023-03-29 11:04:28 +08:00
basefas
f2d019ff97 chore: move some dependencies to devDependencies 2023-03-29 10:53:13 +08:00
Yifei Zhang
3a170d50cb Update README.md 2023-03-29 02:26:14 +08:00
Yifei Zhang
17ee2ee135 Update home.tsx 2023-03-29 02:19:20 +08:00
Yifei Zhang
07956486b5 Merge pull request #74 from Yidadaa/prompt
feat: #2 add prompt list
2023-03-29 02:13:02 +08:00
Yifei Zhang
e648a59b1f feat: add lint-staged 2023-03-28 18:09:05 +00:00
Yifei Zhang
83400093a2 fixup: i18n for prompts 2023-03-28 17:45:23 +00:00
Yifei Zhang
6782e65fdf feat: #2 add prompt hints 2023-03-28 17:45:23 +00:00
Yifei Zhang
7d5e742ea6 feat: #2 add prompt list 2023-03-28 17:45:23 +00:00
Yifei Zhang
19b511e3f8 Update README.md 2023-03-28 23:13:30 +08:00
Yifei Zhang
cde3cbed21 Merge pull request #109 from HimiCos/main
feat: language support traditional chinese
2023-03-28 23:00:16 +08:00
HimiCos
307be405ac feat: language support traditional chinese 2023-03-28 21:29:30 +08:00
石门
1c017b8ee9 fix: minor fix 2023-03-28 20:14:44 +08:00
石门
48dc2c2295 fix: minor fix 2023-03-28 19:38:18 +08:00
石门
3a3999d73a fix: hide actions when loading 2023-03-28 18:59:03 +08:00
Yifei Zhang
7f3cbaa064 Merge pull request #100 from iSource/fix-docker-access-code
fix: docker access code setting missing
2023-03-28 18:52:46 +08:00
石门
7a5c35baf3 fix: hide actions when loading 2023-03-28 18:48:03 +08:00
iSource
eb72c83b7e fix: docker access code setting missing 2023-03-28 17:55:03 +08:00
Yifei Zhang
e93ea0fa97 Update README.md 2023-03-28 17:21:27 +08:00
Yifei Zhang
3b6f93afdf feat: add one-key setup script 2023-03-28 16:51:23 +08:00
Yifei Zhang
4597a2286a Update README.md 2023-03-28 14:53:07 +08:00
Yifei Zhang
780968979d Update README.md 2023-03-28 13:36:08 +08:00
Yifei Zhang
adc0db4c74 Merge pull request #76 from xiaotianxt/main
feat: CmdEnter config for submitkey, bug fix for auto scrolling
2023-03-28 13:35:36 +08:00
Yifei Zhang
f0dd95a2a3 Merge pull request #87 from Yidadaa/bugfix-0328
fix: light theme code highlight
2023-03-28 13:34:39 +08:00
Yifei Zhang
6155a190ac fix: light theme code highlight 2023-03-28 05:24:51 +00:00
xiaotianxt
493aa8c591 refactor: refocus textarea after btn clicked 2023-03-28 13:02:49 +08:00
xiaotianxt
6c82f804ae fix: Enter key bug 2023-03-28 12:56:36 +08:00
xiaotianxt
a2807c9815 fix(scroll): scroll after click submit button
The behavior of SubmitKey and SubmitButton should be the same
2023-03-28 12:56:36 +08:00
xiaotianxt
d822f333c2 feat(SubmitKey): add MetaEnter option
Add another option for MacOS user who prefer Cmd+Enter
or Linux user who prefer Meta+Enter.
2023-03-28 12:56:36 +08:00
Yifei Zhang
8f498075b9 Merge pull request #81 from iSource/fix-code-copy-button
fix: code copy button position
2023-03-28 11:57:03 +08:00
Yifei Zhang
c4bf6a6383 Merge pull request #84 from iSource/add-robots-txt
feat: add robots.txt
2023-03-28 11:56:47 +08:00
Yifei Zhang
939402b2d9 Merge pull request #85 from AprilNEA/fix-mobile
fix: fix #82, close sidebar after new session
2023-03-28 11:56:29 +08:00
AprilNEA
684a3c41ef fix: fix #82, close sidebar after new session 2023-03-28 11:51:36 +08:00
iSource
306f0850e9 feat: add robots.txt 2023-03-28 11:25:47 +08:00
iSource
55f37248f7 fix: code copy button position 2023-03-28 10:50:50 +08:00
Yifei Zhang
c93a46a02f Update README.md 2023-03-28 02:15:52 +08:00
Yifei Zhang
77a3fdea5f Update README.md 2023-03-27 23:02:39 +08:00
35 changed files with 8769 additions and 5027 deletions

View File

@@ -1,3 +1,4 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"plugins": ["prettier"]
}

4
.gitignore vendored
View File

@@ -34,4 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
dev
dev
public/prompts.json

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

6
.lintstagedrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
"eslint --fix",
"prettier --write"
]
}

10
.prettierrc.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: false,
trailingComma: 'all',
bracketSpacing: true,
arrowParens: 'always',
};

View File

@@ -7,9 +7,9 @@
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) / [微信](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)
[演示 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)
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
[![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)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -22,6 +22,7 @@ One-Click to deploy your own ChatGPT web UI.
- 在 1 分钟内使用 Vercel **免费一键部署**
- 精心设计的 UI响应式设计支持深色模式
- 极快的首屏加载速度(~85kb
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 一键导出聊天记录,完整的 Markdown 支持
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
@@ -31,6 +32,7 @@ One-Click to deploy your own ChatGPT web UI.
- **Deploy for free with one-click** on Vercel in under 1 minute
- Responsive design, and dark mode
- Fast first screen loading speed (~85kb)
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
- Automatically compresses chat history to support long conversations while also saving your tokens
- One-click export all chat history with full Markdown support
@@ -38,7 +40,7 @@ One-Click to deploy your own ChatGPT web UI.
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. 点击右侧按钮开始部署:
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登陆即可,记得在环境变量页填入 API Key
[![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),直接使用 Github 账号登陆即可,记得在环境变量页填入 API Key
3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
@@ -46,7 +48,7 @@ One-Click to deploy your own ChatGPT web UI.
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. Click
[![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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web);
[![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);
3. Enjoy :)
## 保持更新 Keep Updated
@@ -76,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
@@ -86,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
@@ -94,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
点击下方按钮,开始二次开发:
@@ -117,16 +151,8 @@ OPENAI_API_KEY=<your api key here>
### 本地部署 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
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
### 容器部署 Docker Deployment
@@ -134,7 +160,7 @@ how to deploy nextjs project with pm2 and yarn on my ubuntu server, the build co
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 -e OPEN_API_KEY="" -e CODE="" aprilnea/chatgpt-next-web
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
```
## 截图 Screenshots
@@ -159,10 +185,12 @@ If you would like to contribute your API key, you can email it to the author and
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon)
### 贡献者 Contributor
[@AprilNEA](https://github.com/AprilNEA)
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## LICENSE

View File

@@ -14,3 +14,4 @@ export function getAccessCodes(): Set<string> {
}
export const ACCESS_CODES = getAccessCodes();
export const IS_IN_DOCKER = process.env.DOCKER;

View File

@@ -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) {

View File

@@ -1 +0,0 @@
config.ts

View File

@@ -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));
}
}

22
app/api/common.ts Normal file
View File

@@ -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,
});
}

28
app/api/openai/route.ts Normal file
View File

@@ -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);
}

View File

@@ -221,6 +221,14 @@
margin-bottom: 100px;
}
.chat-body-title {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.chat-message {
display: flex;
flex-direction: row;
@@ -292,6 +300,7 @@
position: absolute;
right: 20px;
top: -26px;
left: 100px;
transition: all ease 0.3s;
opacity: 0;
pointer-events: none;
@@ -302,6 +311,7 @@
.chat-message-top-action {
opacity: 0.5;
color: var(--black);
white-space: nowrap;
cursor: pointer;
&:hover {
@@ -333,11 +343,65 @@
.chat-input-panel {
position: absolute;
bottom: 20px;
bottom: 0px;
display: flex;
width: 100%;
padding: 20px;
box-sizing: border-box;
flex-direction: column;
}
@mixin single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.prompt-hints {
min-height: 20px;
width: 100%;
max-height: 50vh;
overflow: auto;
display: flex;
flex-direction: column-reverse;
background-color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
margin-bottom: 10px;
box-shadow: var(--shadow);
.prompt-hint {
color: var(--black);
padding: 6px 10px;
animation: slide-in ease 0.3s;
cursor: pointer;
transition: all ease 0.3s;
border: transparent 1px solid;
margin: 4px;
border-radius: 8px;
&:not(:last-child) {
margin-top: 0;
}
.hint-title {
font-size: 12px;
font-weight: bolder;
@include single-line();
}
.hint-content {
font-size: 12px;
@include single-line();
}
&-selected,
&:hover {
border-color: var(--primary);
}
}
}
.chat-input-panel-inner {
@@ -354,7 +418,7 @@
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 14px;
padding: 10px 14px 50px;
resize: none;
outline: none;
}
@@ -375,7 +439,7 @@
position: absolute;
right: 30px;
bottom: 10px;
bottom: 30px;
}
.export-content {

View File

@@ -1,6 +1,7 @@
"use client";
import { useState, useRef, useEffect, useLayoutEffect } from "react";
import { useDebouncedCallback } from "use-debounce";
import { IconButton } from "./button";
import styles from "./home.module.scss";
@@ -28,6 +29,7 @@ import Locale from "../locales";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { ControllerPool } from "../requests";
import { Prompt, usePromptStore } from "../store/prompt";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -100,7 +102,7 @@ export function ChatList() {
state.currentSessionIndex,
state.selectSession,
state.removeSession,
]
],
);
return (
@@ -131,10 +133,12 @@ function useSubmitHandler() {
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
(config.submitKey === SubmitKey.Enter &&
!e.altKey &&
!e.ctrlKey &&
!e.shiftKey)
!e.shiftKey &&
!e.metaKey)
);
};
@@ -144,25 +148,98 @@ function useSubmitHandler() {
};
}
export function Chat(props: { showSideBar?: () => void }) {
export function PromptHints(props: {
prompts: Prompt[];
onPromptSelect: (prompt: Prompt) => void;
}) {
if (props.prompts.length === 0) return null;
return (
<div className={styles["prompt-hints"]}>
{props.prompts.map((prompt, i) => (
<div
className={styles["prompt-hint"]}
key={prompt.title + i.toString()}
onClick={() => props.onPromptSelect(prompt)}
>
<div className={styles["hint-title"]}>{prompt.title}</div>
<div className={styles["hint-content"]}>{prompt.content}</div>
</div>
))}
</div>
);
}
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
}) {
type RenderMessage = Message & { preview?: boolean };
const chatStore = useChatStore();
const [session, sessionIndex] = useChatStore((state) => [
state.currentSession(),
state.currentSessionIndex,
]);
const fontSize = useChatStore((state) => state.config.fontSize);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const onUserInput = useChatStore((state) => state.onUserInput);
// prompt hints
const promptStore = usePromptStore();
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
const onSearch = useDebouncedCallback(
(text: string) => {
setPromptHints(promptStore.search(text));
},
100,
{ leading: true, trailing: true },
);
const onPromptSelect = (prompt: Prompt) => {
setUserInput(prompt.content);
setPromptHints([]);
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) => {
scrollInput();
setUserInput(text);
const n = text.trim().length;
// clear search results
if (n === 0) {
setPromptHints([]);
} 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));
}
}
};
// submit user input
const onUserSubmit = () => {
if (userInput.length <= 0) return;
setIsLoading(true);
onUserInput(userInput).then(() => setIsLoading(false));
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
setUserInput("");
inputRef.current?.focus();
};
// stop response
@@ -195,7 +272,10 @@ export function Chat(props: { showSideBar?: () => void }) {
for (let i = botIndex; i >= 0; i -= 1) {
if (messages[i].role === "user") {
setIsLoading(true);
onUserInput(messages[i].content).then(() => setIsLoading(false));
chatStore
.onUserInput(messages[i].content)
.then(() => setIsLoading(false));
inputRef.current?.focus();
return;
}
}
@@ -219,7 +299,7 @@ export function Chat(props: { showSideBar?: () => void }) {
preview: true,
},
]
: []
: [],
)
.concat(
userInput.length > 0
@@ -231,7 +311,7 @@ export function Chat(props: { showSideBar?: () => void }) {
preview: true,
},
]
: []
: [],
);
// auto scroll
@@ -240,7 +320,6 @@ export function Chat(props: { showSideBar?: () => void }) {
const dom = latestMessageRef.current;
if (dom && !isIOS() && autoScroll) {
dom.scrollIntoView({
behavior: "smooth",
block: "end",
});
}
@@ -254,7 +333,17 @@ export function Chat(props: { showSideBar?: () => void }) {
className={styles["window-header-title"]}
onClick={props?.showSideBar}
>
<div className={styles["window-header-main-title"]}>
<div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClick={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession(
(session) => (session.topic = newTopic!),
);
}
}}
>
{session.topic}
</div>
<div className={styles["window-header-sub-title"]}>
@@ -314,38 +403,40 @@ export function Chat(props: { showSideBar?: () => void }) {
</div>
)}
<div className={styles["chat-message-item"]}>
{!isUser && (
<div className={styles["chat-message-top-actions"]}>
{message.streaming ? (
<div
className={styles["chat-message-top-action"]}
onClick={() => onUserStop(i)}
>
{Locale.Chat.Actions.Stop}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
>
{Locale.Chat.Actions.Retry}
</div>
)}
{!isUser &&
!(message.preview || message.content.length === 0) && (
<div className={styles["chat-message-top-actions"]}>
{message.streaming ? (
<div
className={styles["chat-message-top-action"]}
onClick={() => onUserStop(i)}
>
{Locale.Chat.Actions.Stop}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
>
{Locale.Chat.Actions.Retry}
</div>
)}
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
</div>
</div>
</div>
)}
)}
{(message.preview || message.content.length === 0) &&
!isUser ? (
<LoadingIcon />
) : (
<div
className="markdown-body"
style={{ fontSize: `${fontSize}px` }}
onContextMenu={(e) => onRightClick(e, message)}
>
<Markdown content={message.content} />
@@ -363,23 +454,28 @@ export function Chat(props: { showSideBar?: () => void }) {
</div>
);
})}
<div ref={latestMessageRef} style={{ opacity: 0, height: "2em" }}>
<div ref={latestMessageRef} style={{ opacity: 0, height: "4em" }}>
-
</div>
</div>
<div className={styles["chat-input-panel"]}>
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
<div className={styles["chat-input-panel-inner"]}>
<textarea
ref={inputRef}
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
rows={3}
onInput={(e) => setUserInput(e.currentTarget.value)}
rows={4}
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={(e) => onInputKeyDown(e as any)}
onFocus={() => setAutoScroll(true)}
onBlur={() => setAutoScroll(false)}
autoFocus
onBlur={() => {
setAutoScroll(false);
setTimeout(() => setPromptHints([]), 100);
}}
autoFocus={!props?.sideBarShowing}
/>
<IconButton
icon={<SendWhiteIcon />}
@@ -406,9 +502,11 @@ function useSwitchTheme() {
document.body.classList.add("light");
}
const themeColor = getComputedStyle(document.body).getPropertyValue("--theme-color").trim();
const themeColor = getComputedStyle(document.body)
.getPropertyValue("--theme-color")
.trim();
const metaDescription = document.querySelector('meta[name="theme-color"]');
metaDescription?.setAttribute('content', themeColor);
metaDescription?.setAttribute("content", themeColor);
}, [config.theme]);
}
@@ -486,7 +584,7 @@ export function Home() {
state.newSession,
state.currentSessionIndex,
state.removeSession,
]
],
);
const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true);
@@ -561,7 +659,10 @@ export function Home() {
<IconButton
icon={<AddIcon />}
text={Locale.Home.NewChat}
onClick={createNewSession}
onClick={() => {
createNewSession();
setShowSideBar(false);
}}
/>
</div>
</div>
@@ -576,7 +677,11 @@ export function Home() {
}}
/>
) : (
<Chat key="chat" showSideBar={() => setShowSideBar(true)} />
<Chat
key="chat"
showSideBar={() => setShowSideBar(true)}
sideBarShowing={showSideBar}
/>
)}
</div>
</div>

View File

@@ -7,8 +7,9 @@ import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import CloseIcon from "../icons/close.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
import { List, ListItem, Popover } from "./ui-lib";
import { List, ListItem, Popover, showToast } from "./ui-lib";
import { IconButton } from "./button";
import {
@@ -19,12 +20,14 @@ import {
useUpdateStore,
useAccessStore,
} from "../store";
import { Avatar } from "./home";
import { Avatar, PromptHints } from "./home";
import Locale, { changeLang, getLang } from "../locales";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
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;
@@ -52,7 +55,7 @@ export function Settings(props: { closeSettings: () => void }) {
state.updateConfig,
state.resetConfig,
state.clearAllData,
]
],
);
const updateStore = useUpdateStore();
@@ -68,16 +71,40 @@ 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();
const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0;
return (
<>
<div className={styles["window-header"]}>
@@ -173,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),
);
}}
>
@@ -193,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),
);
}}
>
@@ -206,22 +233,38 @@ export function Settings(props: { closeSettings: () => void }) {
</ListItem>
<SettingItem title={Locale.Settings.Lang.Name}>
<div className="">
<select
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
}}
>
<option value="en" key="en">
{Locale.Settings.Lang.Options.en}
<select
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
}}
>
{AllLangs.map((lang) => (
<option value={lang} key={lang}>
{Locale.Settings.Lang.Options[lang]}
</option>
))}
</select>
</SettingItem>
<option value="cn" key="cn">
{Locale.Settings.Lang.Options.cn}
</option>
</select>
</div>
<SettingItem
title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle}
>
<input
type="range"
title={`${config.fontSize ?? 14}px`}
value={config.fontSize}
min="12"
max="18"
step="1"
onChange={(e) =>
updateConfig(
(config) =>
(config.fontSize = Number.parseInt(e.currentTarget.value)),
)
}
></input>
</SettingItem>
<div className="no-mobile">
@@ -231,13 +274,44 @@ export function Settings(props: { closeSettings: () => void }) {
checked={config.tightBorder}
onChange={(e) =>
updateConfig(
(config) => (config.tightBorder = e.currentTarget.checked)
(config) => (config.tightBorder = e.currentTarget.checked),
)
}
></input>
</SettingItem>
</div>
</List>
<List>
<SettingItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
/>
</SettingItem>
</List>
<List>
{enabledAccessControl ? (
<SettingItem
@@ -271,6 +345,28 @@ export function Settings(props: { closeSettings: () => void }) {
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Usage.Title}
subTitle={
loadingUsage
? Locale.Settings.Usage.IsChecking
: Locale.Settings.Usage.SubTitle(
usage?.granted ?? "[?]",
usage?.used ?? "[?]",
)
}
>
{loadingUsage ? (
<div />
) : (
<IconButton
icon={<ResetIcon></ResetIcon>}
text={Locale.Settings.Usage.Check}
onClick={checkUsage}
/>
)}
</SettingItem>
<SettingItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
@@ -285,7 +381,7 @@ export function Settings(props: { closeSettings: () => void }) {
onChange={(e) =>
updateConfig(
(config) =>
(config.historyMessageCount = e.target.valueAsNumber)
(config.historyMessageCount = e.target.valueAsNumber),
)
}
></input>
@@ -304,7 +400,7 @@ export function Settings(props: { closeSettings: () => void }) {
updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber)
e.currentTarget.valueAsNumber),
)
}
></input>
@@ -317,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),
);
}}
>
@@ -336,13 +433,13 @@ export function Settings(props: { closeSettings: () => void }) {
type="range"
value={config.modelConfig.temperature.toFixed(1)}
min="0"
max="1"
max="2"
step="0.1"
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.temperature =
e.currentTarget.valueAsNumber)
e.currentTarget.valueAsNumber),
);
}}
></input>
@@ -360,7 +457,7 @@ export function Settings(props: { closeSettings: () => void }) {
updateConfig(
(config) =>
(config.modelConfig.max_tokens =
e.currentTarget.valueAsNumber)
e.currentTarget.valueAsNumber),
)
}
></input>
@@ -379,7 +476,7 @@ export function Settings(props: { closeSettings: () => void }) {
updateConfig(
(config) =>
(config.modelConfig.presence_penalty =
e.currentTarget.valueAsNumber)
e.currentTarget.valueAsNumber),
);
}}
></input>

View File

@@ -36,7 +36,7 @@ export function ListItem(props: { children: JSX.Element[] }) {
return <div className={styles["list-item"]}>{props.children}</div>;
}
export function List(props: { children: JSX.Element[] }) {
export function List(props: { children: JSX.Element[] | JSX.Element }) {
return <div className={styles.list}>{props.children}</div>;
}

1
app/icons/edit.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(10.5 11) rotate(0 1.4166666666666665 1.8333333333333333)" d="M2.83,0L2.83,3C2.83,3.37 2.53,3.67 2.17,3.67L0,3.67 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2.6666666666666665 1.3333333333333333) rotate(0 5.333333333333333 6.666666666666666)" d="M10.67,4L10.67,0.67C10.67,0.3 10.37,0 10,0L0.67,0C0.3,0 0,0.3 0,0.67L0,12.67C0,13.03 0.3,13.33 0.67,13.33L2.67,13.33 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 5.333333333333333) rotate(0 2.333333333333333 0)" d="M0,0L4.67,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7.666666666666666 7.666666666666666) rotate(0 2.833333333333333 3.5)" d="M0,7L5.67,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.333333333333333 8) rotate(0 1.3333333333333333 0)" d="M0,0L2.67,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -3,7 +3,7 @@ import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/prism.scss";
import process from "child_process";
import { ACCESS_CODES } from "./api/access";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
let COMMIT_ID: string | undefined;
try {
@@ -28,7 +28,7 @@ export const metadata = {
function Meta() {
const metas = {
version: COMMIT_ID ?? "unknown",
access: ACCESS_CODES.size > 0 ? "enabled" : "disabled",
access: (ACCESS_CODES.size > 0 || IS_IN_DOCKER) ? "enabled" : "disabled",
};
return (

View File

@@ -1,3 +1,5 @@
import { SubmitKey } from "../store/app";
const cn = {
WIP: "该功能仍在开发中……",
Error: {
@@ -16,8 +18,15 @@ const cn = {
Stop: "停止",
Retry: "重试",
},
Rename: "重命名对话",
Typing: "正在输入…",
Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
Input: (submitKey: string) => {
var inputHints = `输入消息,${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行";
}
return inputHints;
},
Send: "发送",
},
Export: {
@@ -45,11 +54,17 @@ const cn = {
Lang: {
Name: "Language",
Options: {
cn: "中文",
cn: "简体中文",
en: "English",
tw: "繁體中文",
},
},
Avatar: "头像",
FontSize: {
Title: "字体大小",
SubTitle: "聊天内容的字体大小",
},
Update: {
Version: (x: string) => `当前版本:${x}`,
IsLatest: "已是最新版本",
@@ -61,6 +76,16 @@ const cn = {
SendKey: "发送键",
Theme: "主题",
TightBorder: "紧凑边框",
Prompt: {
Disable: {
Title: "禁用提示词自动补全",
SubTitle: "在输入框开头输入 / 即可触发自动补全",
},
List: "自定义提示词列表",
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom}`,
Edit: "编辑",
},
HistoryCount: {
Title: "附带历史消息数",
SubTitle: "每次请求携带的历史消息数",
@@ -74,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: "现在是受控访问状态",
@@ -101,7 +134,7 @@ const cn = {
History: (content: string) =>
"这是 ai 和用户的历史聊天总结作为前情提要:" + content,
Topic:
"直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
Summarize:
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 50 字以内",
},

View File

@@ -1,3 +1,4 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const en: LocaleType = {
@@ -19,9 +20,15 @@ const en: LocaleType = {
Stop: "Stop",
Retry: "Retry",
},
Rename: "Rename Chat",
Typing: "Typing…",
Input: (submitKey: string) =>
`Type something and press ${submitKey} to send`,
Input: (submitKey: string) => {
var inputHints = `Type something and press ${submitKey} to send`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", press Shift + Enter to newline";
}
return inputHints;
},
Send: "Send",
},
Export: {
@@ -49,11 +56,16 @@ const en: LocaleType = {
Lang: {
Name: "语言",
Options: {
cn: "中文",
cn: "简体中文",
en: "English",
tw: "繁體中文",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Font Size",
SubTitle: "Adjust font size of chat content",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Latest version",
@@ -65,6 +77,16 @@ const en: LocaleType = {
SendKey: "Send Key",
Theme: "Theme",
TightBorder: "Tight Border",
Prompt: {
Disable: {
Title: "Disable auto-completion",
SubTitle: "Input / to trigger auto-completion",
},
List: "Prompt List",
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
},
HistoryCount: {
Title: "Attached Messages Count",
SubTitle: "Number of sent messages attached per request",
@@ -79,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",
@@ -108,7 +138,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.",
},

View File

@@ -1,53 +1,57 @@
import CN from './cn'
import EN from './en'
import CN from "./cn";
import EN from "./en";
import TW from "./tw";
export type { LocaleType } from './cn'
export type { LocaleType } from "./cn";
type Lang = 'en' | 'cn'
export const AllLangs = ["en", "cn", "tw"] as const;
type Lang = (typeof AllLangs)[number];
const LANG_KEY = 'lang'
const LANG_KEY = "lang";
function getItem(key: string) {
try {
return localStorage.getItem(key)
} catch {
return null
}
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
function setItem(key: string, value: string) {
try {
localStorage.setItem(key, value)
} catch { }
try {
localStorage.setItem(key, value);
} catch {}
}
function getLanguage() {
try {
return navigator.language.toLowerCase()
} catch {
return 'cn'
}
try {
return navigator.language.toLowerCase();
} catch {
return "cn";
}
}
export function getLang(): Lang {
const savedLang = getItem(LANG_KEY)
const savedLang = getItem(LANG_KEY);
if (['en', 'cn'].includes(savedLang ?? '')) {
return savedLang as Lang
}
if (AllLangs.includes((savedLang ?? "") as Lang)) {
return savedLang as Lang;
}
const lang = getLanguage()
const lang = getLanguage();
if (lang.includes('zh') || lang.includes('cn')) {
return 'cn'
} else {
return 'en'
}
if (lang.includes("zh") || lang.includes("cn")) {
return "cn";
} else if (lang.includes("tw")) {
return "tw";
} else {
return "en";
}
}
export function changeLang(lang: Lang) {
setItem(LANG_KEY, lang)
location.reload()
setItem(LANG_KEY, lang);
location.reload();
}
export default { en: EN, cn: CN }[getLang()]
export default { en: EN, cn: CN, tw: TW }[getLang()];

148
app/locales/tw.ts Normal file
View File

@@ -0,0 +1,148 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const tw: LocaleType = {
WIP: "該功能仍在開發中……",
Error: {
Unauthorized: "目前您的狀態是未授權,請前往設定頁面填寫授權碼。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 條對話`,
},
Chat: {
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
Actions: {
ChatList: "查看消息列表",
CompressedHistory: "查看壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
Copy: "複製",
Stop: "停止",
Retry: "重試",
},
Rename: "重命名對話",
Typing: "正在輸入…",
Input: (submitKey: string) => {
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 鍵換行";
}
return inputHints;
},
Send: "發送",
},
Export: {
Title: "匯出聊天記錄為 Markdown",
Copy: "複製全部",
Download: "下載檔案",
},
Memory: {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",
Copy: "複製全部",
},
Home: {
NewChat: "新的對話",
DeleteChat: "確定要刪除選取的對話嗎?",
},
Settings: {
Title: "設定",
SubTitle: "設定選項",
Actions: {
ClearAll: "清除所有數據",
ResetAll: "重置所有設定",
Close: "關閉",
},
Lang: {
Name: "語言",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
},
},
Avatar: "大頭貼",
FontSize: {
Title: "字型大小",
SubTitle: "聊天內容的字型大小",
},
Update: {
Version: (x: string) => `當前版本:${x}`,
IsLatest: "已是最新版本",
CheckUpdate: "檢查更新",
IsChecking: "正在檢查更新...",
FoundUpdate: (x: string) => `發現新版本:${x}`,
GoToUpdate: "前往更新",
},
SendKey: "發送鍵",
Theme: "主題",
TightBorder: "緊湊邊框",
Prompt: {
Disable: {
Title: "停用提示詞自動補全",
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補全",
},
List: "自定義提示詞列表",
ListCount: (builtin: number, custom: number) =>
`內置 ${builtin} 條,用戶定義 ${custom}`,
Edit: "編輯",
},
HistoryCount: {
Title: "附帶歷史訊息數",
SubTitle: "每次請求附帶的歷史訊息數",
},
CompressThreshold: {
Title: "歷史訊息長度壓縮閾值",
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
},
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可規避受控訪問限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "帳戶餘額",
SubTitle(granted: any, used: any) {
return `總共 $${granted},已使用 $${used}`;
},
IsChecking: "正在檢查…",
Check: "重新檢查",
},
AccessCode: {
Title: "訪問碼",
SubTitle: "現在是受控訪問狀態",
Placeholder: "請輸入訪問碼",
},
Model: "模型 (model)",
Temperature: {
Title: "隨機性 (temperature)",
SubTitle: "值越大,回復越隨機",
},
MaxTokens: {
Title: "單次回復限制 (max_tokens)",
SubTitle: "單次交互所用的最大 Token 數",
},
PresencePenlty: {
Title: "話題新穎度 (presence_penalty)",
SubTitle: "值越大,越有可能擴展到新話題",
},
},
Store: {
DefaultTopic: "新的對話",
BotHello: "請問需要我的協助嗎?",
Error: "出錯了,請稍後再嘗試",
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
Summarize:
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt且字數控制在 50 字以內",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
Copy: {
Success: "已複製到剪貼簿中",
Failed: "複製失敗,請賦予剪貼簿權限",
},
};
export default tw;

View File

@@ -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;

View File

@@ -21,6 +21,7 @@ export enum SubmitKey {
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
@@ -30,15 +31,17 @@ export enum Theme {
}
export interface ChatConfig {
maxToken?: number;
historyMessageCount: number; // -1 means all
compressMessageLengthThreshold: number;
sendBotMessages: boolean; // send bot's message or not
submitKey: SubmitKey;
avatar: string;
fontSize: number;
theme: Theme;
tightBorder: boolean;
disablePromptHint: boolean;
modelConfig: {
model: string;
temperature: number;
@@ -120,9 +123,12 @@ const DEFAULT_CONFIG: ChatConfig = {
sendBotMessages: true as boolean,
submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: false,
disablePromptHint: false,
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
@@ -189,7 +195,7 @@ interface ChatStore {
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void
updater: (message?: Message) => void,
) => void;
getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message;
@@ -200,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<ChatStore>()(
@@ -337,7 +347,7 @@ export const useChatStore = create<ChatStore>()(
ControllerPool.addController(
sessionIndex,
messageIndex,
controller
controller,
);
},
filterBot: !get().config.sendBotMessages,
@@ -360,7 +370,7 @@ export const useChatStore = create<ChatStore>()(
const config = get().config;
const n = session.messages.length;
const recentMessages = session.messages.slice(
n - config.historyMessageCount
n - config.historyMessageCount,
);
const memoryPrompt = get().getMemoryPrompt();
@@ -375,7 +385,7 @@ export const useChatStore = create<ChatStore>()(
updateMessage(
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void
updater: (message?: Message) => void,
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
@@ -387,29 +397,30 @@ export const useChatStore = create<ChatStore>()(
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(
(session) => (session.topic = trimTopic(res))
(session) => (session.topic = trimTopic(res)),
);
}
},
);
}
const config = get().config;
let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex
);
const historyMsgLength = toBeSummarizedMsgs.reduce(
(pre, cur) => pre + cur.content.length,
0
session.lastSummarizeIndex,
);
const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > 4000) {
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
-config.historyMessageCount
-config.historyMessageCount,
);
}
@@ -422,7 +433,7 @@ export const useChatStore = create<ChatStore>()(
"[Chat History] ",
toBeSummarizedMsgs,
historyMsgLength,
config.compressMessageLengthThreshold
config.compressMessageLengthThreshold,
);
if (historyMsgLength > config.compressMessageLengthThreshold) {
@@ -444,7 +455,7 @@ export const useChatStore = create<ChatStore>()(
onError(error) {
console.error("[Summarize] ", error);
},
}
},
);
}
},
@@ -473,6 +484,6 @@ export const useChatStore = create<ChatStore>()(
{
name: LOCAL_KEY,
version: 1,
}
)
},
),
);

117
app/store/prompt.ts Normal file
View File

@@ -0,0 +1,117 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
export interface Prompt {
id?: number;
title: string;
content: string;
}
export interface PromptStore {
latestId: number;
prompts: Map<number, Prompt>;
add: (prompt: Prompt) => number;
remove: (id: number) => void;
search: (text: string) => Prompt[];
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
engine: new Fuse<Prompt>([], { keys: ["title"] }),
count: {
builtin: 0,
},
init(prompts: Prompt[]) {
if (this.ready) {
return;
}
this.engine.setCollection(prompts);
this.ready = true;
},
remove(id: number) {
this.engine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
this.engine.add(prompt);
},
search(text: string) {
const results = this.engine.search(text);
return results.map((v) => v.item);
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
latestId: 0,
prompts: new Map(),
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
prompts.set(prompt.id, prompt);
set(() => ({
latestId: prompt.id!,
prompts: prompts,
}));
return prompt.id!;
},
remove(id) {
const prompts = get().prompts;
prompts.delete(id);
SearchService.remove(id);
set(() => ({
prompts,
}));
},
search(text) {
return SearchService.search(text) as Prompt[];
},
}),
{
name: PROMPT_KEY,
version: 1,
onRehydrateStorage(state) {
const PROMPT_URL = "./prompts.json";
type PromptList = Array<[string, string]>;
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
const builtinPrompts = [res.en, res.cn]
.map((promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
title,
content,
} as Prompt)
);
})
.concat([...(state?.prompts?.values() ?? [])]);
const allPromptsForSearch = builtinPrompts.reduce(
(pre, cur) => pre.concat(cur),
[]
);
SearchService.count.builtin = res.en.length + res.cn.length;
SearchService.init(allPromptsForSearch);
});
},
}
)
);

View File

@@ -839,21 +839,20 @@
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
padding: 16px 16px 8px 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
display: inline-block;
max-width: 100%;
padding: 0;
margin: 0;
overflow: visible;
overflow-x: scroll;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
@@ -1117,4 +1116,4 @@
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
}

View File

@@ -121,32 +121,32 @@
}
}
// @mixin light {
// .markdown-body pre[class*="language-"] {
// filter: invert(1) hue-rotate(50deg) brightness(1.3);
// }
// }
@mixin light {
.markdown-body pre {
filter: invert(1) hue-rotate(90deg) brightness(1.3);
}
}
// @mixin dark {
// .markdown-body pre[class*="language-"] {
// filter: none;
// }
// }
@mixin dark {
.markdown-body pre {
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();
}
}

View File

@@ -3,10 +3,10 @@ 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, 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,40 @@ 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,
}
},
);
}
return NextResponse.next();
// inject api key
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(
{
error: true,
msg: "Empty Api Key",
},
{
status: 401,
},
);
}
} else {
console.log("[Auth] set user token");
}
return NextResponse.next({
request: {
headers: req.headers,
},
});
}

View File

@@ -4,25 +4,21 @@
"private": false,
"license": "Anti 996",
"scripts": {
"dev": "next dev",
"build": "next build",
"dev": "yarn fetch && next dev",
"build": "yarn fetch && next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"fetch": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install"
},
"dependencies": {
"@svgr/webpack": "^6.5.1",
"@types/node": "^18.14.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2",
"@vercel/analytics": "^0.1.11",
"cross-env": "^7.0.3",
"emoji-picker-react": "^4.4.7",
"eslint": "8.35.0",
"eslint-config-next": "13.2.3",
"eventsource-parser": "^0.1.0",
"fuse.js": "^6.6.2",
"next": "^13.2.3",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -33,7 +29,23 @@
"remark-math": "^5.1.1",
"sass": "^1.59.2",
"spark-md5": "^3.0.2",
"typescript": "4.9.5",
"use-debounce": "^9.0.3",
"zustand": "^4.3.6"
},
"devDependencies": {
"@types/node": "^18.14.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2",
"cross-env": "^7.0.3",
"eslint": "^8.36.0",
"eslint-config-next": "13.2.3",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.0",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"typescript": "4.9.5"
}
}

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Disallow: /
User-agent: vitals.vercel-insights.com
Allow: /

View File

@@ -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);
})
);
});

49
scripts/fetch-prompts.mjs Normal file
View File

@@ -0,0 +1,49 @@
import fetch from "node-fetch";
import fs from "fs/promises";
const CN_URL =
"https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
const EN_URL =
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
const FILE = "./public/prompts.json";
async function fetchCN() {
console.log("[Fetch] fetching cn prompts...");
try {
const raw = await (await fetch(CN_URL)).json();
return raw.map((v) => [v.act, v.prompt]);
} catch (error) {
console.error("[Fetch] failed to fetch cn prompts", error);
return [];
}
}
async function fetchEN() {
console.log("[Fetch] fetching en prompts...");
try {
const raw = await (await fetch(EN_URL)).text();
return raw
.split("\n")
.slice(1)
.map((v) => v.split('","').map((v) => v.replace('"', "")));
} catch (error) {
console.error("[Fetch] failed to fetch cn prompts", error);
return [];
}
}
async function main() {
Promise.all([fetchCN(), fetchEN()])
.then(([cn, en]) => {
fs.writeFile(FILE, JSON.stringify({ cn, en }));
})
.catch((e) => {
console.error("[Fetch] failed to fetch prompts");
fs.writeFile(FILE, JSON.stringify({ cn: [], en: [] }));
})
.finally(() => {
console.log("[Fetch] saved to " + FILE);
});
}
main();

64
scripts/setup.sh Normal file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Check if running on a supported system
case "$(uname -s)" in
Linux)
if [[ -f "/etc/lsb-release" ]]; then
. /etc/lsb-release
if [[ "$DISTRIB_ID" != "Ubuntu" ]]; then
echo "This script only works on Ubuntu, not $DISTRIB_ID."
exit 1
fi
else
if [[ ! "$(cat /etc/*-release | grep '^ID=')" =~ ^(ID=\"ubuntu\")|(ID=\"centos\")|(ID=\"arch\")$ ]]; then
echo "Unsupported Linux distribution."
exit 1
fi
fi
;;
Darwin)
echo "Running on MacOS."
;;
*)
echo "Unsupported operating system."
exit 1
;;
esac
# Check if needed dependencies are installed and install if necessary
if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then
case "$(uname -s)" in
Linux)
if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"ubuntu\"" ]]; then
sudo apt-get update
sudo apt-get -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"centos\"" ]]; then
sudo yum -y install epel-release
sudo yum -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"arch\"" ]]; then
sudo pacman -Syu -y
sudo pacman -S -y nodejs git yarn
else
echo "Unsupported Linux distribution"
exit 1
fi
;;
Darwin)
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install node git yarn
;;
esac
fi
# Clone the repository and install dependencies
git clone https://github.com/Yidadaa/ChatGPT-Next-Web
cd ChatGPT-Next-Web
yarn install
# Prompt user for environment variables
read -p "Enter OPENAI_API_KEY: " OPENAI_API_KEY
read -p "Enter CODE: " CODE
read -p "Enter PORT: " PORT
# Build and run the project using the environment variables
OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build && OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start

12391
yarn.lock

File diff suppressed because it is too large Load Diff