Compare commits

..

66 Commits

Author SHA1 Message Date
Yifei Zhang
3c6f2962cc Merge pull request #617 from Yidadaa/bugfix-0408
fix: auto grow textarea
2023-04-08 03:13:14 +08:00
Yidadaa
45c8de42b9 fix: #528 wont send max_tokens 2023-04-08 03:03:07 +08:00
Yidadaa
637cda5b4c fix: #613 show all prompts when input / 2023-04-08 02:53:35 +08:00
Yidadaa
13035ecb0d fix: auto grow textarea 2023-04-08 02:36:02 +08:00
Yifei Zhang
d6b2cf8bcb Merge pull request #318 from leedom92/textarea-height-optimize
feat: textarea with adaptive height
2023-04-08 01:01:52 +08:00
Yifei Zhang
9afed21efd Merge pull request #595 from yorunning/fix
fix: hide toast when confirmation box is cancelled
2023-04-08 00:59:09 +08:00
Yorun
71d9fbc367 fix: hide toast on cancel session deletion on mobile 2023-04-07 14:51:08 +08:00
Yifei Zhang
f83859113e Update README.md 2023-04-07 13:58:56 +08:00
leedom
00d45e7cc4 fix: solve navigator undefined && merge from main 2023-04-07 13:47:34 +08:00
leedom
406ed8a02a Merge branch 'main' into textarea-height-optimize 2023-04-07 13:33:37 +08:00
Yifei Zhang
e793b0c49d Merge pull request #587 from zhongmeizhi/main
feat: 添加对热键的支持 Escape to close settings, Up Arrow to get last input
2023-04-07 13:09:09 +08:00
leedom
de740ec57f feat: add calcTextareaHeight.ts 2023-04-07 12:42:47 +08:00
leedom
e5b4cb28fe fix: useEffect hooks 2023-04-07 12:39:27 +08:00
leedom
cb210d82e1 update: resizeTextarea 2023-04-07 12:33:09 +08:00
Mokou
58b956f7cc feat: Hot keys: Escape to close settings, Up Arrow to get last input 2023-04-07 12:26:22 +08:00
leedom
620b98fe6a Merge branch 'main' into textarea-height-optimize 2023-04-07 12:20:53 +08:00
leedom
3656c8458f feat: textarea with adaptive height 2023-04-07 12:17:37 +08:00
Yifei Zhang
fec055d8a8 Merge pull request #583 from Yidadaa/Yidadaa-patch-2
fix: #537 delete chat button style
2023-04-07 11:26:35 +08:00
Yifei Zhang
6420f61566 fix: #537 delete chat button style 2023-04-07 11:23:10 +08:00
Yifei Zhang
905bf41cd8 Update utils.ts 2023-04-07 11:08:20 +08:00
Yifei Zhang
0acb877b03 Merge pull request #576 from XGZepto/main
Add FAQ-EN.
2023-04-07 10:54:13 +08:00
leedom
a8a8becf96 merge 2023-04-07 07:29:17 +08:00
leedom
b8a605f07d merge 2023-04-07 07:18:53 +08:00
Zepto
52f80e0c44 Refine China Specific Issues 2023-04-07 02:59:36 +08:00
Zepto
a28bfdbaf8 Add FAQ-EN 2023-04-06 18:53:11 +00:00
Yifei Zhang
4d675c11e8 Update faq-cn.md 2023-04-07 00:58:22 +08:00
Yifei Zhang
cc56121e67 Merge pull request #574 from Yidadaa/bugfix0406
fix: toast, renaming and revert delete session
2023-04-07 00:50:43 +08:00
Yidadaa
5952064362 feat: #499 revert delete session 2023-04-07 00:14:27 +08:00
Yidadaa
806587c8ea fix: #512 Mobile renaming should not return to chat list 2023-04-06 23:47:47 +08:00
Yidadaa
f3dbe5a251 fix: #513 show toast after copying 2023-04-06 23:18:51 +08:00
Yifei Zhang
74cff2639b Merge pull request #567 from leedom92/optimize-send-button
refactor: optimize component `IconButton`
2023-04-06 23:10:09 +08:00
leedom
cd671066f7 remove unnecessary judgment 2023-04-06 22:52:18 +08:00
Leedom
fb3f5a414a Update button.module.scss 2023-04-06 21:34:00 +08:00
Yifei Zhang
9a952f0e45 Merge pull request #565 from xiaotianxt/main
fix: distinguish PC/Mobile behavior on auto-scroll
2023-04-06 21:24:25 +08:00
Yifei Zhang
cd3a11b16f Merge pull request #566 from latorc/main
docs: 更新中文FAQ faq-cn.md
2023-04-06 21:24:02 +08:00
leedom
b7cdea1b82 refactor: optimize send button 2023-04-06 21:02:48 +08:00
latorc
2092f30af5 Update faq-cn.md 2023-04-06 20:55:54 +08:00
xiaotianxt
dd20c36a55 fix: distinguish PC/Mobile behavior on auto-scroll
The chat list should be set to auto-scroll on mobile screen when the
input textarea is focused. It should not behave like that on PC screen
because user may want to refer to previous content.
2023-04-06 20:38:21 +08:00
Yifei Zhang
85bf4ac077 fix: #559 custom input ui style 2023-04-06 18:16:49 +08:00
Yifei Zhang
29bc9a45df Merge pull request #535 from zhongmeizhi/main
fix: 兼容不同浏览器的input range
2023-04-06 17:47:05 +08:00
Yifei Zhang
09fde0528a Update README.md 2023-04-06 17:38:03 +08:00
Yifei Zhang
f7624f29b1 Merge pull request #557 from Yidadaa/Yidadaa-patch-1
Update requests.ts
2023-04-06 17:35:37 +08:00
Yifei Zhang
f7e42179d0 fixup 2023-04-06 17:34:17 +08:00
Yifei Zhang
6823839f4b fixup 2023-04-06 17:28:09 +08:00
Yifei Zhang
a68721fcf2 Update requests.ts 2023-04-06 17:14:19 +08:00
Yifei Zhang
03a2a4e534 Update README.md 2023-04-06 17:02:47 +08:00
Mokou
d92108453f fix: 兼容不同浏览器的input range兼容 2023-04-06 12:39:31 +08:00
Yifei Zhang
03b3f16472 Update README_CN.md 2023-04-06 04:50:32 +08:00
Yifei Zhang
6bd2c2f121 Create CODE_OF_CONDUCT.md 2023-04-06 04:43:31 +08:00
Yifei Zhang
998c5b2969 Update issue templates 2023-04-06 04:27:38 +08:00
Yifei Zhang
f509cc73ca Update issue templates 2023-04-06 04:17:20 +08:00
Yifei Zhang
f71354faed Merge pull request #509 from xiaotianxt/feat/dnd-xiaotianxt
Drag & Drop support for ChatList
2023-04-06 04:12:00 +08:00
Yifei Zhang
796eafbf8f Update README_CN.md 2023-04-06 04:11:12 +08:00
Yifei Zhang
46daafd190 Update README_CN.md 2023-04-06 04:10:51 +08:00
xiaotianxt
f920b2001d performance: introduce lazy-loading for ChatList
Reduce the first load JS bundle size using next/dynamic.
2023-04-06 02:41:27 +08:00
xiaotianxt
301cbbfdfb feat(dnd): add drag and drop feature 2023-04-06 01:43:10 +08:00
xiaotianxt
3490c294dc chore(deps): introduce dnd 2023-04-06 01:43:10 +08:00
leedom
3825b3c2c2 Merge branch 'textarea-height-optimize' of github.com:leedom92/ChatGPT-Next-Web into textarea-height-optimize 2023-04-02 11:16:30 +08:00
Leedom
4369b26e22 Update calcTextareaHeight.js 2023-04-02 09:47:55 +08:00
leedom
6f7c2916ef Merge remote-tracking branch 'upstream/main' into textarea-height-optimize 2023-04-02 03:20:06 +08:00
Leedom
b419e7d918 Update home.tsx 2023-04-02 02:56:45 +08:00
Leedom
ad8e09d188 Update home.tsx 2023-04-02 02:54:46 +08:00
leedom
bce020fc8e feat: add calcTextareaHeight.js from element-ui 2023-04-02 02:42:00 +08:00
leedom
a811637176 refactor: use the method of element-ui 2023-04-02 02:40:00 +08:00
Leedom
9ba59351c5 Update home.tsx 2023-04-01 17:48:05 +08:00
leedom
2a79d35667 feat: add optimize textarea height when inputing 2023-04-01 17:34:38 +08:00
34 changed files with 918 additions and 159 deletions

View File

@@ -1,7 +1,7 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: "[Bug] "
labels: '' labels: ''
assignees: '' assignees: ''
@@ -23,7 +23,7 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
** Deployment **Deployment**
- [ ] Docker - [ ] Docker
- [ ] Vercel - [ ] Vercel
- [ ] Server - [ ] Server

View File

@@ -1,7 +1,7 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: "[Feature] "
labels: '' labels: ''
assignees: '' assignees: ''

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
flynn.zhang@foxmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -3,11 +3,11 @@
<h1 align="center">ChatGPT Next Web</h1> <h1 align="center">ChatGPT Next Web</h1>
One-Click to deploy your own ChatGPT web UI. One-Click to deploy well-designed ChatGPT web UI on Vercel.
一键免费部署你的私人 ChatGPT 网页应用。 一键免费部署你的私人 ChatGPT 网页应用。
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join 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) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
[![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) [![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)
@@ -71,9 +71,7 @@ One-Click to deploy your own ChatGPT web UI.
3. Enjoy :) 3. Enjoy :)
## FAQ ## FAQ
> [简体中文 > 常见问题](./docs/faq-cn.md) [简体中文 > 常见问题](./docs/faq-cn.md) | [English > FAQ](./docs/faq.en.md)
We are sorry that there is currently no English version of the FAQ. English users can use translation tools to access the document. We look forward to receiving your PR for an English version of the documentation.
## Keep Updated ## Keep Updated
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新) > [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
@@ -171,6 +169,8 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
![More](./docs/images/more.png) ![More](./docs/images/more.png)
## Donation
[Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
## Special Thanks ## Special Thanks
### Sponsor ### Sponsor
@@ -179,6 +179,11 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
[@ClarenceDan](https://github.com/ClarenceDan) [@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia) [@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon) [@hoochanlon](https://github.com/hoochanlon)
[@relativequantum](https://github.com/relativequantum)
[@desenmeng](https://github.com/desenmeng)
[@webees](https://github.com/webees)
[@chazzhou](https://github.com/chazzhou)
[@hauy](https://github.com/hauy)
### Contributor ### Contributor

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="./static/icon.svg" alt="预览"/> <img src="./docs/images/icon.svg" alt="预览"/>
<h1 align="center">ChatGPT Next Web</h1> <h1 align="center">ChatGPT Next Web</h1>
@@ -90,7 +90,7 @@ OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此
## 开发 ## 开发
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,导致很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器
点击下方按钮,开始二次开发: 点击下方按钮,开始二次开发:
@@ -125,13 +125,6 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
``` ```
## 截图 Screenshots
![设置](./static/settings.png)
![更多展示](./static/more.png)
## 鸣谢 ## 鸣谢
### 捐赠者 ### 捐赠者
> 仅列出了部分大额打赏,小额打赏(< 100RMB人数太多在此不再列出敬请谅解。 > 仅列出了部分大额打赏,小额打赏(< 100RMB人数太多在此不再列出敬请谅解。

View File

@@ -6,6 +6,7 @@ async function makeRequest(req: NextRequest) {
const api = await requestOpenai(req); const api = await requestOpenai(req);
const res = new NextResponse(api.body); const res = new NextResponse(api.body);
res.headers.set("Content-Type", "application/json"); res.headers.set("Content-Type", "application/json");
res.headers.set("Cache-Control", "no-cache");
return res; return res;
} catch (e) { } catch (e) {
console.error("[OpenAI] ", req.body, e); console.error("[OpenAI] ", req.body, e);

View File

@@ -10,6 +10,14 @@
transition: all 0.3s ease; transition: all 0.3s ease;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
outline: none;
border: none;
color: var(--black);
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
} }
.shadow { .shadow {

View File

@@ -11,9 +11,10 @@ export function IconButton(props: {
noDark?: boolean; noDark?: boolean;
className?: string; className?: string;
title?: string; title?: string;
disabled?: boolean;
}) { }) {
return ( return (
<div <button
className={ className={
styles["icon-button"] + styles["icon-button"] +
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${ ` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
@@ -22,6 +23,7 @@ export function IconButton(props: {
} }
onClick={props.onClick} onClick={props.onClick}
title={props.title} title={props.title}
disabled={props.disabled}
role="button" role="button"
> >
<div <div
@@ -32,6 +34,6 @@ export function IconButton(props: {
{props.text && ( {props.text && (
<div className={styles["icon-button-text"]}>{props.text}</div> <div className={styles["icon-button-text"]}>{props.text}</div>
)} )}
</div> </button>
); );
} }

View File

@@ -1,14 +1,13 @@
import { useState, useRef, useEffect, useLayoutEffect } from "react";
import DeleteIcon from "../icons/delete.svg"; import DeleteIcon from "../icons/delete.svg";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
import { import {
Message, DragDropContext,
SubmitKey, Droppable,
useChatStore, Draggable,
ChatSession, OnDragEndResponder,
BOT_HELLO, } from "@hello-pangea/dnd";
} from "../store";
import { useChatStore } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import { isMobileScreen } from "../utils"; import { isMobileScreen } from "../utils";
@@ -20,54 +19,90 @@ export function ChatItem(props: {
count: number; count: number;
time: string; time: string;
selected: boolean; selected: boolean;
id: number;
index: number;
}) { }) {
return ( return (
<div <Draggable draggableId={`${props.id}`} index={props.index}>
className={`${styles["chat-item"]} ${ {(provided) => (
props.selected && styles["chat-item-selected"] <div
}`} className={`${styles["chat-item"]} ${
onClick={props.onClick} props.selected && styles["chat-item-selected"]
> }`}
<div className={styles["chat-item-title"]}>{props.title}</div> onClick={props.onClick}
<div className={styles["chat-item-info"]}> ref={provided.innerRef}
<div className={styles["chat-item-count"]}> {...provided.draggableProps}
{Locale.ChatItem.ChatItemCount(props.count)} {...provided.dragHandleProps}
>
<div className={styles["chat-item-title"]}>{props.title}</div>
<div className={styles["chat-item-info"]}>
<div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
<div className={styles["chat-item-date"]}>{props.time}</div>
</div>
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
<DeleteIcon />
</div>
</div> </div>
<div className={styles["chat-item-date"]}>{props.time}</div> )}
</div> </Draggable>
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
<DeleteIcon />
</div>
</div>
); );
} }
export function ChatList() { export function ChatList() {
const [sessions, selectedIndex, selectSession, removeSession] = useChatStore( const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
(state) => [ useChatStore((state) => [
state.sessions, state.sessions,
state.currentSessionIndex, state.currentSessionIndex,
state.selectSession, state.selectSession,
state.removeSession, state.removeSession,
], state.moveSession,
); ]);
const chatStore = useChatStore();
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
moveSession(source.index, destination.index);
};
return ( return (
<div className={styles["chat-list"]}> <DragDropContext onDragEnd={onDragEnd}>
{sessions.map((item, i) => ( <Droppable droppableId="chat-list">
<ChatItem {(provided) => (
title={item.topic} <div
time={item.lastUpdate} className={styles["chat-list"]}
count={item.messages.length} ref={provided.innerRef}
key={i} {...provided.droppableProps}
selected={i === selectedIndex} >
onClick={() => selectSession(i)} {sessions.map((item, i) => (
onDelete={() => <ChatItem
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) && title={item.topic}
removeSession(i) time={item.lastUpdate}
} count={item.messages.length}
/> key={item.id}
))} id={item.id}
</div> index={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={chatStore.deleteSession}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
); );
} }

View File

@@ -1,10 +1,10 @@
import { useDebouncedCallback } from "use-debounce"; import { useDebounce, useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
import ExportIcon from "../icons/export.svg"; import ExportIcon from "../icons/export.svg";
import MenuIcon from "../icons/menu.svg"; import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg"; import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
@@ -27,6 +27,7 @@ import {
getEmojiUrl, getEmojiUrl,
isMobileScreen, isMobileScreen,
selectOrCopy, selectOrCopy,
autoGrowTextArea,
} from "../utils"; } from "../utils";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
@@ -39,7 +40,7 @@ import { IconButton } from "./button";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss"; import chatStyle from "./chat.module.scss";
import { Input, Modal, showModal, showToast } from "./ui-lib"; import { Input, Modal, showModal } from "./ui-lib";
const Markdown = dynamic( const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown), async () => memo((await import("./markdown")).Markdown),
@@ -343,6 +344,7 @@ export function Chat(props: {
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState(""); const [userInput, setUserInput] = useState("");
const [beforeInput, setBeforeInput] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler(); const { submitKey, shouldSubmit } = useSubmitHandler();
const { scrollRef, setAutoScroll } = useScrollToBottom(); const { scrollRef, setAutoScroll } = useScrollToBottom();
@@ -380,6 +382,27 @@ export function Chat(props: {
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum; dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
}; };
// auto grow input
const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback(
() => {
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
const inputRows = Math.min(
5,
Math.max(2 + Number(!isMobileScreen()), rows),
);
setInputRows(inputRows);
},
100,
{
leading: true,
trailing: true,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(measure, [userInput]);
// only search prompts when user input is short // only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30; const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => { const onInput = (text: string) => {
@@ -394,9 +417,6 @@ export function Chat(props: {
// check if need to trigger auto completion // check if need to trigger auto completion
if (text.startsWith("/")) { if (text.startsWith("/")) {
let searchText = text.slice(1); let searchText = text.slice(1);
if (searchText.length === 0) {
searchText = " ";
}
onSearch(searchText); onSearch(searchText);
} }
} }
@@ -407,6 +427,7 @@ export function Chat(props: {
if (userInput.length <= 0) return; if (userInput.length <= 0) return;
setIsLoading(true); setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false)); chatStore.onUserInput(userInput).then(() => setIsLoading(false));
setBeforeInput(userInput);
setUserInput(""); setUserInput("");
setPromptHints([]); setPromptHints([]);
if (!isMobileScreen()) inputRef.current?.focus(); if (!isMobileScreen()) inputRef.current?.focus();
@@ -420,6 +441,12 @@ export function Chat(props: {
// check if should send message // check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// if ArrowUp and no userInput
if (e.key === "ArrowUp" && userInput.length <= 0) {
setUserInput(beforeInput);
e.preventDefault();
return;
}
if (shouldSubmit(e)) { if (shouldSubmit(e)) {
onUserSubmit(); onUserSubmit();
e.preventDefault(); e.preventDefault();
@@ -507,13 +534,10 @@ export function Chat(props: {
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={props?.showSideBar}
>
<div <div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`} className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClick={() => { onClickCapture={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic); const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) { if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
@@ -531,7 +555,7 @@ export function Chat(props: {
<div className={styles["window-actions"]}> <div className={styles["window-actions"]}>
<div className={styles["window-action-button"] + " " + styles.mobile}> <div className={styles["window-action-button"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<MenuIcon />} icon={<ReturnIcon />}
bordered bordered
title={Locale.Chat.Actions.ChatList} title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar} onClick={props?.showSideBar}
@@ -663,7 +687,6 @@ export function Chat(props: {
ref={inputRef} ref={inputRef}
className={styles["chat-input"]} className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)} placeholder={Locale.Chat.Input(submitKey)}
rows={2}
onInput={(e) => onInput(e.currentTarget.value)} onInput={(e) => onInput(e.currentTarget.value)}
value={userInput} value={userInput}
onKeyDown={onInputKeyDown} onKeyDown={onInputKeyDown}
@@ -673,6 +696,7 @@ export function Chat(props: {
setTimeout(() => setPromptHints([]), 500); setTimeout(() => setPromptHints([]), 500);
}} }}
autoFocus={!props?.sideBarShowing} autoFocus={!props?.sideBarShowing}
rows={inputRows}
/> />
<IconButton <IconButton
icon={<SendWhiteIcon />} icon={<SendWhiteIcon />}

View File

@@ -125,7 +125,7 @@
border-radius: 10px; border-radius: 10px;
margin-bottom: 10px; margin-bottom: 10px;
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
transition: all 0.3s ease; transition: background-color 0.3s ease;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
border: 2px solid transparent; border: 2px solid transparent;
@@ -157,6 +157,7 @@
right: -20px; right: -20px;
transition: all ease 0.3s; transition: all ease 0.3s;
opacity: 0; opacity: 0;
cursor: pointer;
} }
.chat-item:hover > .chat-item-delete { .chat-item:hover > .chat-item-delete {
@@ -405,17 +406,11 @@
background-color: var(--white); background-color: var(--white);
color: var(--black); color: var(--black);
font-family: inherit; font-family: inherit;
padding: 10px 14px 50px; padding: 10px 90px 10px 14px;
resize: none; resize: none;
outline: none; outline: none;
} }
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
}
.chat-input:focus { .chat-input:focus {
border: 1px solid var(--primary); border: 1px solid var(--primary);
} }
@@ -426,7 +421,17 @@
position: absolute; position: absolute;
right: 30px; right: 30px;
bottom: 30px; bottom: 32px;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
.chat-input-send {
bottom: 30px;
}
} }
.export-content { .export-content {

View File

@@ -19,7 +19,6 @@ import CloseIcon from "../icons/close.svg";
import { useChatStore } from "../store"; import { useChatStore } from "../store";
import { isMobileScreen } from "../utils"; import { isMobileScreen } from "../utils";
import Locale from "../locales"; import Locale from "../locales";
import { ChatList } from "./chat-list";
import { Chat } from "./chat"; import { Chat } from "./chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
@@ -39,6 +38,10 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />, loading: () => <Loading noLogo />,
}); });
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => <Loading noLogo />,
});
function useSwitchTheme() { function useSwitchTheme() {
const config = useChatStore((state) => state.config); const config = useChatStore((state) => state.config);
@@ -90,6 +93,7 @@ function _Home() {
state.removeSession, state.removeSession,
], ],
); );
const chatStore = useChatStore();
const loading = !useHasHydrated(); const loading = !useHasHydrated();
const [showSideBar, setShowSideBar] = useState(true); const [showSideBar, setShowSideBar] = useState(true);
@@ -139,11 +143,7 @@ function _Home() {
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={() => { onClick={chatStore.deleteSession}
if (confirm(Locale.Home.DeleteChat)) {
removeSession(currentIndex);
}
}}
/> />
</div> </div>
<div className={styles["sidebar-action"]}> <div className={styles["sidebar-action"]}>

View File

@@ -0,0 +1,7 @@
.input-range {
border: var(--border-in-light);
border-radius: 10px;
padding: 5px 15px 5px 10px;
font-size: 12px;
display: flex;
}

View File

@@ -0,0 +1,37 @@
import * as React from "react";
import styles from "./input-range.module.scss";
interface InputRangeProps {
onChange: React.ChangeEventHandler<HTMLInputElement>;
title?: string;
value: number | string;
className?: string;
min: string;
max: string;
step: string;
}
export function InputRange({
onChange,
title,
value,
className,
min,
max,
step,
}: InputRangeProps) {
return (
<div className={styles["input-range"] + ` ${className ?? ""}`}>
{title || value}
<input
type="range"
title={title}
value={value}
min={min}
max={max}
step={step}
onChange={onChange}
></input>
</div>
);
}

View File

@@ -67,7 +67,7 @@ export function Markdown(props: { content: string }) {
components={{ components={{
pre: PreCode, pre: PreCode,
}} }}
linkTarget={'_blank'} linkTarget={"_blank"}
> >
{props.content} {props.content}
</ReactMarkdown> </ReactMarkdown>

View File

@@ -32,6 +32,7 @@ import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt"; import { SearchService, usePromptStore } from "../store/prompt";
import { requestUsage } from "../requests"; import { requestUsage } from "../requests";
import { ErrorBoundary } from "./error"; import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
function SettingItem(props: { function SettingItem(props: {
title: string; title: string;
@@ -127,6 +128,19 @@ export function Settings(props: { closeSettings: () => void }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
const keydownEvent = (e: KeyboardEvent) => {
if (e.key === "Escape") {
props.closeSettings();
}
};
document.addEventListener("keydown", keydownEvent);
return () => {
document.removeEventListener("keydown", keydownEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<ErrorBoundary> <ErrorBoundary>
<div className={styles["window-header"]}> <div className={styles["window-header"]}>
@@ -274,8 +288,7 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.FontSize.Title} title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle} subTitle={Locale.Settings.FontSize.SubTitle}
> >
<input <InputRange
type="range"
title={`${config.fontSize ?? 14}px`} title={`${config.fontSize ?? 14}px`}
value={config.fontSize} value={config.fontSize}
min="12" min="12"
@@ -287,7 +300,7 @@ export function Settings(props: { closeSettings: () => void }) {
(config.fontSize = Number.parseInt(e.currentTarget.value)), (config.fontSize = Number.parseInt(e.currentTarget.value)),
) )
} }
></input> ></InputRange>
</SettingItem> </SettingItem>
<SettingItem title={Locale.Settings.TightBorder}> <SettingItem title={Locale.Settings.TightBorder}>
@@ -407,8 +420,7 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.HistoryCount.Title} title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle} subTitle={Locale.Settings.HistoryCount.SubTitle}
> >
<input <InputRange
type="range"
title={config.historyMessageCount.toString()} title={config.historyMessageCount.toString()}
value={config.historyMessageCount} value={config.historyMessageCount}
min="0" min="0"
@@ -420,7 +432,7 @@ export function Settings(props: { closeSettings: () => void }) {
(config.historyMessageCount = e.target.valueAsNumber), (config.historyMessageCount = e.target.valueAsNumber),
) )
} }
></input> ></InputRange>
</SettingItem> </SettingItem>
<SettingItem <SettingItem
@@ -467,8 +479,7 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.Temperature.Title} title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle} subTitle={Locale.Settings.Temperature.SubTitle}
> >
<input <InputRange
type="range"
value={config.modelConfig.temperature?.toFixed(1)} value={config.modelConfig.temperature?.toFixed(1)}
min="0" min="0"
max="2" max="2"
@@ -482,7 +493,7 @@ export function Settings(props: { closeSettings: () => void }) {
)), )),
); );
}} }}
></input> ></InputRange>
</SettingItem> </SettingItem>
<SettingItem <SettingItem
title={Locale.Settings.MaxTokens.Title} title={Locale.Settings.MaxTokens.Title}
@@ -508,8 +519,7 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.PresencePenlty.Title} title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle} subTitle={Locale.Settings.PresencePenlty.SubTitle}
> >
<input <InputRange
type="range"
value={config.modelConfig.presence_penalty?.toFixed(1)} value={config.modelConfig.presence_penalty?.toFixed(1)}
min="-2" min="-2"
max="2" max="2"
@@ -523,7 +533,7 @@ export function Settings(props: { closeSettings: () => void }) {
)), )),
); );
}} }}
></input> ></InputRange>
</SettingItem> </SettingItem>
</List> </List>
</div> </div>

View File

@@ -135,9 +135,25 @@
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
border: var(--border-in-light); border: var(--border-in-light);
color: var(--black); color: var(--black);
padding: 10px 30px; padding: 10px 20px;
border-radius: 50px; border-radius: 50px;
margin-bottom: 20px; margin-bottom: 20px;
display: flex;
align-items: center;
.toast-action {
padding-left: 20px;
color: var(--primary);
opacity: 0.8;
border: 0;
background: none;
cursor: pointer;
font-family: inherit;
&:hover {
opacity: 1;
}
}
} }
} }
@@ -149,6 +165,7 @@
background-color: var(--white); background-color: var(--white);
color: var(--black); color: var(--black);
resize: none; resize: none;
min-width: 50px;
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {

View File

@@ -110,17 +110,37 @@ export function showModal(props: ModalProps) {
root.render(<Modal {...props} onClose={closeModal}></Modal>); root.render(<Modal {...props} onClose={closeModal}></Modal>);
} }
export type ToastProps = { content: string }; export type ToastProps = {
content: string;
action?: {
text: string;
onClick: () => void;
};
};
export function Toast(props: ToastProps) { export function Toast(props: ToastProps) {
return ( return (
<div className={styles["toast-container"]}> <div className={styles["toast-container"]}>
<div className={styles["toast-content"]}>{props.content}</div> <div className={styles["toast-content"]}>
<span>{props.content}</span>
{props.action && (
<button
onClick={props.action.onClick}
className={styles["toast-action"]}
>
{props.action.text}
</button>
)}
</div>
</div> </div>
); );
} }
export function showToast(content: string, delay = 3000) { export function showToast(
content: string,
action?: ToastProps["action"],
delay = 3000,
) {
const div = document.createElement("div"); const div = document.createElement("div");
div.className = styles.show; div.className = styles.show;
document.body.appendChild(div); document.body.appendChild(div);
@@ -139,7 +159,7 @@ export function showToast(content: string, delay = 3000) {
close(); close();
}, delay); }, delay);
root.render(<Toast content={content} />); root.render(<Toast content={content} action={action} />);
} }
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & { export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {

21
app/icons/return.svg Normal file
View File

@@ -0,0 +1,21 @@
<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(2 2.6666666666666665) rotate(0 1.1666333333333334 2.1666666666666665)"
d="M2.33,0L0,2L2.33,4.33 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 4.666666666666666) rotate(0 6.000006859869576 4.333333333333333)"
d="M0,0L7.66,0C9.96,0 11.91,1.87 12,4.17C12.09,6.59 10.09,8.67 7.66,8.67L2,8.67 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1013 B

View File

@@ -21,11 +21,11 @@ const cn = {
Rename: "重命名对话", Rename: "重命名对话",
Typing: "正在输入…", Typing: "正在输入…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `输入消息,${submitKey} 发送`; var inputHints = `${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行"; inputHints += "Shift + Enter 换行";
} }
return inputHints; return inputHints + "/ 触发补全";
}, },
Send: "发送", Send: "发送",
}, },
@@ -47,6 +47,8 @@ const cn = {
Home: { Home: {
NewChat: "新的聊天", NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?", DeleteChat: "确认删除选中的对话?",
DeleteToast: "已删除会话",
Revert: "撤销",
}, },
Settings: { Settings: {
Title: "设置", Title: "设置",
@@ -124,7 +126,7 @@ const cn = {
Model: "模型 (model)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机", SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码",
}, },
MaxTokens: { MaxTokens: {
Title: "单次回复限制 (max_tokens)", Title: "单次回复限制 (max_tokens)",

View File

@@ -23,11 +23,11 @@ const en: LocaleType = {
Rename: "Rename Chat", Rename: "Rename Chat",
Typing: "Typing…", Typing: "Typing…",
Input: (submitKey: string) => { Input: (submitKey: string) => {
var inputHints = `Type something and press ${submitKey} to send`; var inputHints = `${submitKey} to send`;
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", press Shift + Enter to newline"; inputHints += ", Shift + Enter to wrap";
} }
return inputHints; return inputHints + ", / to search prompts";
}, },
Send: "Send", Send: "Send",
}, },
@@ -50,6 +50,8 @@ const en: LocaleType = {
Home: { Home: {
NewChat: "New Chat", NewChat: "New Chat",
DeleteChat: "Confirm to delete the selected conversation?", DeleteChat: "Confirm to delete the selected conversation?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
}, },
Settings: { Settings: {
Title: "Settings", Title: "Settings",

View File

@@ -50,6 +50,8 @@ const es: LocaleType = {
Home: { Home: {
NewChat: "Nuevo chat", NewChat: "Nuevo chat",
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?", DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
}, },
Settings: { Settings: {
Title: "Configuración", Title: "Configuración",

View File

@@ -50,6 +50,8 @@ const it: LocaleType = {
Home: { Home: {
NewChat: "Nuova Chat", NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?", DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
DeleteToast: "Chat Deleted",
Revert: "Revert",
}, },
Settings: { Settings: {
Title: "Impostazioni", Title: "Impostazioni",

View File

@@ -48,6 +48,8 @@ const tw: LocaleType = {
Home: { Home: {
NewChat: "新的對話", NewChat: "新的對話",
DeleteChat: "確定要刪除選取的對話嗎?", DeleteChat: "確定要刪除選取的對話嗎?",
DeleteToast: "已刪除對話",
Revert: "撤銷",
}, },
Settings: { Settings: {
Title: "設定", Title: "設定",

View File

@@ -20,7 +20,11 @@ const makeRequestParam = (
sendMessages = sendMessages.filter((m) => m.role !== "assistant"); sendMessages = sendMessages.filter((m) => m.role !== "assistant");
} }
const modelConfig = useChatStore.getState().config.modelConfig; const modelConfig = { ...useChatStore.getState().config.modelConfig };
// @yidadaa: wont send max_tokens, because it is nonsense for Muggles
// @ts-expect-error
delete modelConfig.max_tokens;
return { return {
messages: sendMessages, messages: sendMessages,
@@ -46,11 +50,10 @@ function getHeaders() {
export function requestOpenaiClient(path: string) { export function requestOpenaiClient(path: string) {
return (body: any, method = "POST") => return (body: any, method = "POST") =>
fetch("/api/openai", { fetch("/api/openai?_vercel_no_cache=1", {
method, method,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-cache",
path, path,
...getHeaders(), ...getHeaders(),
}, },
@@ -77,7 +80,7 @@ export async function requestUsage() {
.getDate() .getDate()
.toString() .toString()
.padStart(2, "0")}`; .padStart(2, "0")}`;
const ONE_DAY = 24 * 60 * 60 * 1000; const ONE_DAY = 2 * 24 * 60 * 60 * 1000;
const now = new Date(Date.now() + ONE_DAY); const now = new Date(Date.now() + ONE_DAY);
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth); const startDate = formatDate(startOfMonth);

View File

@@ -7,9 +7,10 @@ import {
requestChatStream, requestChatStream,
requestWithPrompt, requestWithPrompt,
} from "../requests"; } from "../requests";
import { trimTopic } from "../utils"; import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales"; import Locale from "../locales";
import { showToast } from "../components/ui-lib";
export type Message = ChatCompletionResponseMessage & { export type Message = ChatCompletionResponseMessage & {
date: string; date: string;
@@ -201,8 +202,10 @@ interface ChatStore {
currentSessionIndex: number; currentSessionIndex: number;
clearSessions: () => void; clearSessions: () => void;
removeSession: (index: number) => void; removeSession: (index: number) => void;
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void; selectSession: (index: number) => void;
newSession: () => void; newSession: () => void;
deleteSession: () => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
onNewMessage: (message: Message) => void; onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
@@ -291,6 +294,31 @@ export const useChatStore = create<ChatStore>()(
}); });
}, },
moveSession(from: number, to: number) {
set((state) => {
const { sessions, currentSessionIndex: oldIndex } = state;
// move the session
const newSessions = [...sessions];
const session = newSessions[from];
newSessions.splice(from, 1);
newSessions.splice(to, 0, session);
// modify current session id
let newIndex = oldIndex === from ? to : oldIndex;
if (oldIndex > from && oldIndex <= to) {
newIndex -= 1;
} else if (oldIndex < from && oldIndex >= to) {
newIndex += 1;
}
return {
currentSessionIndex: newIndex,
sessions: newSessions,
};
});
},
newSession() { newSession() {
set((state) => ({ set((state) => ({
currentSessionIndex: 0, currentSessionIndex: 0,
@@ -298,6 +326,27 @@ export const useChatStore = create<ChatStore>()(
})); }));
}, },
deleteSession() {
const deletedSession = get().currentSession();
const index = get().currentSessionIndex;
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
showToast(Locale.Home.DeleteToast, {
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(state.sessions.slice(index + Number(isLastSession))),
}));
},
});
}
},
currentSession() { currentSession() {
let index = get().currentSessionIndex; let index = get().currentSessionIndex;
const sessions = get().sessions; const sessions = get().sessions;

View File

@@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { getLang } from "../locales";
export interface Prompt { export interface Prompt {
id?: number; id?: number;
@@ -25,11 +26,13 @@ export const SearchService = {
count: { count: {
builtin: 0, builtin: 0,
}, },
allBuiltInPrompts: [] as Prompt[],
init(prompts: Prompt[]) { init(prompts: Prompt[]) {
if (this.ready) { if (this.ready) {
return; return;
} }
this.allBuiltInPrompts = prompts;
this.engine.setCollection(prompts); this.engine.setCollection(prompts);
this.ready = true; this.ready = true;
}, },
@@ -78,6 +81,11 @@ export const usePromptStore = create<PromptStore>()(
}, },
search(text) { search(text) {
if (text.length === 0) {
// return all prompts
const userPrompts = get().prompts?.values?.() ?? [];
return SearchService.allBuiltInPrompts.concat([...userPrompts]);
}
return SearchService.search(text) as Prompt[]; return SearchService.search(text) as Prompt[];
}, },
}), }),
@@ -92,7 +100,11 @@ export const usePromptStore = create<PromptStore>()(
fetch(PROMPT_URL) fetch(PROMPT_URL)
.then((res) => res.json()) .then((res) => res.json())
.then((res) => { .then((res) => {
const builtinPrompts = [res.en, res.cn] let fetchPrompts = [res.en, res.cn];
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
const builtinPrompts = fetchPrompts
.map((promptList: PromptList) => { .map((promptList: PromptList) => {
return promptList.map( return promptList.map(
([title, content]) => ([title, content]) =>

View File

@@ -159,19 +159,11 @@ input[type="checkbox"]:checked::after {
input[type="range"] { input[type="range"] {
appearance: none; appearance: none;
border: var(--border-in-light);
border-radius: 10px;
padding: 5px 15px 5px 10px;
background-color: var(--white); background-color: var(--white);
color: var(--black); color: var(--black);
&::before {
content: attr(value);
font-size: 12px;
}
} }
input[type="range"]::-webkit-slider-thumb { @mixin thumb() {
appearance: none; appearance: none;
height: 8px; height: 8px;
width: 20px; width: 20px;
@@ -180,11 +172,36 @@ input[type="range"]::-webkit-slider-thumb {
cursor: pointer; cursor: pointer;
transition: all ease 0.3s; transition: all ease 0.3s;
margin-left: 5px; margin-left: 5px;
border: none;
}
input[type="range"]::-webkit-slider-thumb {
@include thumb();
}
input[type="range"]::-moz-range-thumb {
@include thumb();
}
input[type="range"]::-ms-thumb {
@include thumb();
}
@mixin thumbHover() {
transform: scaleY(1.2);
width: 24px;
} }
input[type="range"]::-webkit-slider-thumb:hover { input[type="range"]::-webkit-slider-thumb:hover {
transform: scaleY(1.2); @include thumbHover();
width: 24px; }
input[type="range"]::-moz-range-thumb:hover {
@include thumbHover();
}
input[type="range"]::-ms-thumb:hover {
@include thumbHover();
} }
input[type="number"], input[type="number"],

View File

@@ -7,21 +7,20 @@ export function trimTopic(topic: string) {
} }
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {
if (navigator.clipboard) { try {
navigator.clipboard.writeText(text).catch(err => { await navigator.clipboard.writeText(text);
console.error('Failed to copy: ', err); showToast(Locale.Copy.Success);
}); } catch (error) {
} else { const textArea = document.createElement("textarea");
const textArea = document.createElement('textarea');
textArea.value = text; textArea.value = text;
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.focus(); textArea.focus();
textArea.select(); textArea.select();
try { try {
document.execCommand('copy'); document.execCommand("copy");
console.log('Text copied to clipboard'); showToast(Locale.Copy.Success);
} catch (err) { } catch (error) {
console.error('Failed to copy: ', err); showToast(Locale.Copy.Failed);
} }
document.body.removeChild(textArea); document.body.removeChild(textArea);
} }
@@ -52,6 +51,12 @@ export function isMobileScreen() {
return window.innerWidth <= 600; return window.innerWidth <= 600;
} }
export function isFirefox() {
return (
typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent)
);
}
export function selectOrCopy(el: HTMLElement, content: string) { export function selectOrCopy(el: HTMLElement, content: string) {
const currentSelection = window.getSelection(); const currentSelection = window.getSelection();
@@ -92,3 +97,51 @@ export function getCurrentVersion() {
export function getEmojiUrl(unified: string, style: EmojiStyle) { export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
} }
function getDomContentWidth(dom: HTMLElement) {
const style = window.getComputedStyle(dom);
const paddingWidth =
parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
const width = dom.clientWidth - paddingWidth;
return width;
}
function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) {
let dom = document.getElementById(id);
if (!dom) {
dom = document.createElement("span");
dom.style.position = "absolute";
dom.style.wordBreak = "break-word";
dom.style.fontSize = "14px";
dom.style.transform = "translateY(-200vh)";
dom.style.pointerEvents = "none";
dom.style.opacity = "0";
dom.id = id;
document.body.appendChild(dom);
init?.(dom);
}
return dom!;
}
export function autoGrowTextArea(dom: HTMLTextAreaElement) {
const measureDom = getOrCreateMeasureDom("__measure");
const singleLineDom = getOrCreateMeasureDom("__single_measure", (dom) => {
dom.innerText = "TEXT_FOR_MEASURE";
});
const width = getDomContentWidth(dom);
measureDom.style.width = width + "px";
measureDom.innerHTML = dom.value.trim().length > 0 ? dom.value : "1";
const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1);
const height = parseFloat(window.getComputedStyle(measureDom).height);
const singleLineHeight = parseFloat(
window.getComputedStyle(singleLineDom).height,
);
const rows = Math.round(height / singleLineHeight) + lineWrapCount;
return rows;
}

View File

@@ -1,7 +1,21 @@
# 常见问题 # 常见问题
> We are sorry that there is currently no English version of the FAQ. English users can use translation tools to access this document. We look forward to receiving your PR for an English version of the documentation. > We are sorry that there is currently no English version of the FAQ. English users can use translation tools to access this document. We look forward to receiving your PR for an English version of the documentation.
<a name="change-env-vars" /> ## 如何快速获得帮助?
1. 询问ChatGPT / Bing / 百度 / Google等。
2. 询问网友。请提供问题的背景信息和碰到问题的详细描述。高质量的提问容易获得有用的答案。
# 部署相关问题
## 为什么 Docker 部署版本一直提示更新
Docker 版本相当于稳定版latest Docker 总是与 latest release version 一致,目前我们的发版频率是一到两天发一次,所以 Docker 版本会总是落后最新的提交一到两天,这在预期内。
## 如何部署在Vercel上
1. 注册Github账号fork该项目
2. 注册Vercel需手机验证可以用中国号码连接你的Github账户
3. Vercel上新建项目选择你在Github fork的项目按需填写环境变量开始部署。部署之后你可以在有梯子的条件下通过vercel提供的域名访问你的项目。
4. 如果需要在国内无墙访问在你的域名管理网站添加一条域名的CNAME记录指向cname.vercel-dns.com。之后在Vercel上设置你的域名访问。
## 如何修改 Vercel 环境变量 ## 如何修改 Vercel 环境变量
- 进入 vercel 的控制台页面; - 进入 vercel 的控制台页面;
@@ -9,8 +23,11 @@
- 点击页面头部的 Settings 选项; - 点击页面头部的 Settings 选项;
- 找到侧边栏的 Environment Variables 选项; - 找到侧边栏的 Environment Variables 选项;
- 修改对应的值即可。 - 修改对应的值即可。
<a name="no-stream" /> ## 环境变量CODE是什么必须设置吗
这是你自定义的访问密码,你可以选择:
1. 不设置,删除该环境变量即可。谨慎:此时任何人可以访问你的项目。
2. 部署项目时设置环境变量CODE支持多个密码逗号分隔。设置访问密码后用户需要在设置界面输入访问密码才可以使用。参见[相关说明](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81)
## 为什么我部署的版本没有流式响应 ## 为什么我部署的版本没有流式响应
> 相关讨论:[#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386) > 相关讨论:[#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
@@ -28,7 +45,15 @@ keepalive_timeout 300; # 设定keep-alive超时时间为65秒
如果你是在 netlify 部署,此问题依然等待解决,请耐心等待。 如果你是在 netlify 部署,此问题依然等待解决,请耐心等待。
<a name="error" /> ## 我部署好了,但是无法访问
请检查排除以下问题:
- 服务启动了吗?
- 端口正确映射了吗?
- 防火墙开放端口了吗?
- 到服务器的路由通吗?
- 域名正确解析了吗?
# 使用相关问题
## 为什么会一直提示“出错了,稍后重试吧” ## 为什么会一直提示“出错了,稍后重试吧”
原因可能有很多,请依次排查: 原因可能有很多,请依次排查:
@@ -37,16 +62,77 @@ keepalive_timeout 300; # 设定keep-alive超时时间为65秒
- 请检查 api key 是否可用; - 请检查 api key 是否可用;
- 如果经历了上述步骤依旧无法确定问题,请在 issue 区提交一个新 issue并附上 vercel 的 runtime log 或者 docker 运行时的 log。 - 如果经历了上述步骤依旧无法确定问题,请在 issue 区提交一个新 issue并附上 vercel 的 runtime log 或者 docker 运行时的 log。
<a name="docker-update" />
## 为什么 Docker 部署版本一直提示更新
Docker 版本相当于稳定版latest Docker 总是与 latest release version 一致,目前我们的发版频率是一到两天发一次,所以 Docker 版本会总是落后最新的提交一到两天,这在预期内。
<a name="random-response" />
## 为什么 ChatGPT 的回复会乱码 ## 为什么 ChatGPT 的回复会乱码
设置界面 - 模型设置项中,有一项为 `temperature`,如果此值大于 1那么就有可能造成回复乱码将其调回 1 以内即可。 设置界面 - 模型设置项中,有一项为 `temperature`,如果此值大于 1那么就有可能造成回复乱码将其调回 1 以内即可。
## 使用时提示“现在是未授权状态,请在设置页输入访问密码”?
项目通过环境变量CODE设置了访问密码。第一次使用时需要到设置中输入访问码才可以使用。
## 使用时提示"You exceeded your current quota, ..."
API KEY有问题。余额不足。
## 什么是代理,如何使用?
由于OpenAI的IP限制中国和其他一些国家/地区无法直接连接OpenAI API需要通过代理。你可以使用代理服务器正向代理或者已经设置好的OpenAI API反向代理。
- 正向代理例子科学上网梯子。docker部署的情况下设置环境变量HTTP_PROXY为你的代理地址http://地址:端口)。
- 反向代理例子可以用别人搭建的代理地址或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。
## 国内服务器可以部署吗?
可以但需要解决的问题:
- 需要代理才能连接github和openAI等网站
- 国内服务器要设置域名解析的话需要备案;
- 国内政策限制代理访问外网/ChatGPT相关应用可能被封。
# 网络服务相关问题
## Cloudflare是什么
CloudflareCF是一个提供CDN域名管理静态页面托管边缘计算函数部署等的网络服务供应商。常见的用途购买和/或托管你的域名解析、动态域名等给你的服务器套上CDN可以隐藏ip免被墙部署网站CF Pages。CF免费提供大多数服务。
## Vercel是什么
Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建和部署现代 Web 应用程序。本项目以及许多Web应用可以一键免费部署在Vercel上。无需懂代码无需懂linux无需服务器无需付费无需设置OpenAI API代理。缺点是需要绑定域名才可以在国内无墙访问。
## 如何获得一个域名?
1. 自己去域名供应商处注册国外有Namesilo支持支付宝, Cloudflare等等国内有万网等等
2. 免费的域名供应商eu.org(二级域名)等;
3. 问朋友要一个免费的二级域名。
## 如何获得一台服务器
- 国外服务器供应商举例亚马逊云谷歌云VultrBandwagonHostdare等等
国外服务器事项服务器线路影响国内访问速度推荐CN2 GIA和CN2线路的服务器。若服务器在国内访问困难丢包严重等可以尝试套CDNCloudflare等供应商
- 国内服务器供应商:阿里云,腾讯等;
国内服务器事项解析域名需要备案国内服务器带宽较贵访问国外网站Github, openAI等需要代理。
# OpenAI相关问题
## 如何注册OpenAI账号
去chat.openai.com注册。你需要
- 一个良好的梯子OpenAI支持地区原生IP地址
- 一个支持的邮箱例如Gmail或者公司/学校邮箱非Outlook或qq邮箱
- 接收短信认证的方式例如SMS-activate网站
## 怎么开通OpenAI API? 怎么查询API余额
官网地址需梯子https://platform.openai.com/account/usage
有网友搭建了无需梯子的余额查询代理请询问网友获取。请鉴别来源是否可靠以免API Key泄露。
## 我新注册的OpenAI账号怎么没有API余额
4月6日更新新注册账号通常会在24小时后显示API余额。当前新注册账号赠送5美元余额。
## 如何给OpenAI API充值
OpenAI只接受指定地区的信用卡中国信用卡无法使用。一些途径举例
1. Depay虚拟信用卡
2. 申请国外信用卡
3. 网上找人代充
## 如何使用GPT-4的API访问
4月6日更新GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist准备好你的OpenAI组织IDhttps://openai.com/waitlist/gpt-4-api
之后等待邮件消息。
## 如何使用 Azure OpenAI 接口 ## 如何使用 Azure OpenAI 接口
请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371) 请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
## 为什么我的 Token 消耗得这么快?
> 相关讨论:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
- 如果你有 GPT 4 的权限,并且日常在使用 GPT 4 api那么由于 GPT 4 价格是 GPT 3.5 的 15 倍左右,你的账单金额会急速膨胀;
- 如果你在使用 GPT 3.5,并且使用频率并不高,仍然发现自己的账单金额在飞快增加,那么请马上按照以下步骤排查:
- 去 openai 官网查看你的 api key 消费记录,如果你的 token 每小时都有消费,并且每次都消耗了上万 token那你的 key 一定是泄露了,请立即删除重新生成。**不要在乱七八糟的网站上查余额。**
- 如果你的密码设置很短,比如 5 位以内的字母,那么爆破成本是非常低的,建议你搜索一下 docker 的日志记录确认是否有人大量尝试了密码组合关键字got access code
- 通过上述两个方法就可以定位到你的 token 被快速消耗的原因:
- 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露;
- 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。

136
docs/faq.en.md Normal file
View File

@@ -0,0 +1,136 @@
# Frequently Asked Questions
## How to get help quickly?
1. Ask ChatGPT / Bing / Baidu / Google, etc.
2. Ask online friends. Please provide background information and a detailed description of the problem. High-quality questions are more likely to get useful answers.
# Deployment Related Questions
## Why does the Docker deployment version always prompt for updates
The Docker version is equivalent to the stable version, and the latest Docker is always consistent with the latest release version. Currently, our release frequency is once every one to two days, so the Docker version will always be one to two days behind the latest commit, which is expected.
## How to deploy on Vercel
1. Register a Github account and fork this project.
2. Register Vercel (mobile phone verification required, Chinese number can be used), and connect your Github account.
3. Create a new project on Vercel, select the project you forked on Github, fill in the required environment variables, and start deploying. After deployment, you can access your project through the domain provided by Vercel. (Requires proxy in mainland China)
* If you need to access it directly in China: At your DNS provider, add a CNAME record for the domain name, pointing to cname.vercel-dns.com. Then set up your domain access on Vercel.
## How to modify Vercel environment variables
- Enter the Vercel console page;
- Select your chatgpt-next-web project;
- Click on the Settings option at the top of the page;
- Find the Environment Variables option in the sidebar;
- Modify the corresponding values as needed.
## What is the environment variable CODE? Is it necessary to set it?
This is your custom access password, you can choose:
1. Do not set it, delete the environment variable. Be cautious: anyone can access your project at this time.
2. When deploying the project, set the environment variable CODE (supports multiple passwords, separated by commas). After setting the access password, users need to enter the access password in the settings page to use it. See [related instructions](https://github.com/Yidadaa/ChatGPT-Next-Web#access-password)
## Why doesn't the version I deployed have streaming response
> Related discussion: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
If you use nginx reverse proxy, you need to add the following code to the configuration file:
```
# No caching, support streaming output
proxy_cache off; # Turn off caching
proxy_buffering off; # Turn off proxy buffering
chunked_transfer_encoding on; # Turn on chunked transfer encoding
tcp_nopush on; # Turn on TCP NOPUSH option, disable Nagle algorithm
tcp_nodelay on; # Turn on TCP NODELAY option, disable delay ACK algorithm
keepalive_timeout 300; # Set keep-alive timeout to 65 seconds
```
If you are deploying on netlify, this issue is still waiting to be resolved, please be patient.
## I've deployed, but it's not accessible
Please check and troubleshoot the following issues:
- Is the service started?
- Is the port correctly mapped?
- Is the firewall port open?
- Is the route to the server okay?
- Is the domain name resolved correctly?
# Usage Related Questions
## Why does it always prompt "An error occurred, please try again later"
There could be many reasons, please check the following in order:
- First, check if your code version is the latest version, update to the latest version and try again;
- Check if the api key is set correctly, the environment variable name must be uppercase with underscores;
- Check if the api key is available;
- If you still cannot determine the problem after going through the above steps, please submit a new issue in the issue area and attach the runtime log of vercel or the log of docker runtime.
## Why does ChatGPT's reply get garbled
In the settings page - model settings, there is an item called `temperature`. If this value is greater than 1, it may cause garbled replies. Adjust it back to within 1.
## It prompts "Now it's unauthorized, please enter the access password on the settings page" when using?
The project has set an access password through the environment variable CODE. When using it for the first time, you need to go to settings and enter the access code to use.
## It prompts "You exceeded your current quota, ..." when using?
The API KEY is problematic. Insufficient balance.
## What is a proxy and how to use it?
Due to IP restrictions of OpenAI, China and some other countries/regions cannot directly connect to OpenAI API and need to go through a proxy. You can use a proxy server (forward proxy) or a pre-configured OpenAI API reverse proxy.
- Forward proxy example: VPN ladder. In the case of docker deployment, set the environment variable HTTP_PROXY to your proxy address (http://address:port).
- Reverse proxy example: You can use someone else's proxy address or set it up for free through Cloudflare. Set the project environment variable BASE_URL to your proxy address.
## Can I deploy it on a server in China?
It is possible but there are issues to be addressed:
- Proxy is required to connect to websites such as Github and OpenAI;
- Domain name resolution requires filing for servers in China;
- Chinese policy restricts proxy access to foreign websites/ChatGPT-related applications, which may be blocked.
# Network Service Related Questions
## What is Cloudflare?
Cloudflare (CF) is a network service provider offering CDN, domain management, static page hosting, edge computing function deployment, and more. Common use cases: purchase and/or host your domain (resolution, dynamic domain, etc.), apply CDN to your server (can hide IP to avoid being blocked), deploy websites (CF Pages). CF offers most services for free.
## What is Vercel?
Vercel is a global cloud platform designed to help developers build and deploy modern web applications more quickly. This project and many web applications can be deployed on Vercel with a single click for free. No need to understand code, Linux, have a server, pay, or set up an OpenAI API proxy. The downside is that you need to bind a domain name to access it without restrictions in China.
## How to obtain a domain name?
1. Register with a domain provider, such as Namesilo (supports Alipay) or Cloudflare for international providers, and Wanwang for domestic providers in China.
2. Free domain name providers: eu.org (second-level domain), etc.
3. Ask friends for a free second-level domain.
## How to obtain a server
- Examples of international server providers: Amazon Web Services, Google Cloud, Vultr, Bandwagon, Hostdare, etc.
International server considerations: Server lines affect access speed in China; CN2 GIA and CN2 lines are recommended. If the server has difficulty accessing in China (serious packet loss, etc.), you can try using a CDN (from providers like Cloudflare).
- Domestic server providers: Alibaba Cloud, Tencent, etc.
Domestic server considerations: Domain name resolution requires filing; domestic server bandwidth is relatively expensive; accessing foreign websites (Github, OpenAI, etc.) requires a proxy.
# OpenAI-related Questions
## How to register an OpenAI account?
Go to chat.openai.com to register. You will need:
- A good VPN (OpenAI only allows native IP addresses of supported regions)
- A supported email (e.g., Gmail or a company/school email, not Outlook or QQ email)
- A way to receive SMS verification (e.g., SMS-activate website)
## How to activate OpenAI API? How to check API balance?
Official website (requires VPN): https://platform.openai.com/account/usage
Some users have set up a proxy to check the balance without a VPN; ask online friends for access. Please verify the source is reliable to avoid API Key leakage.
## Why doesn't my new OpenAI account have an API balance?
(Updated April 6th) Newly registered accounts usually display API balance within 24 hours. New accounts are currently given a $5 balance.
## How to recharge OpenAI API?
OpenAI only accepts credit cards from designated regions (Chinese credit cards cannot be used). If the credit cards from your region is not supported, some options include:
1. Depay virtual credit card
2. Apply for a foreign credit card
3. Find someone online to top up
## How to access the GPT-4 API?
(Updated April 6th) Access to the GPT-4 API requires a separate application. Go to the following address and enter your information to join the waitlist (prepare your OpenAI organization ID): https://openai.com/waitlist/gpt-4-api
Wait for email updates afterwards.
## How to use the Azure OpenAI interface
Please refer to: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
## Why is my Token consumed so fast?
> Related discussion: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
- If you have GPT-4 access and use GPT-4 API regularly, your bill will increase rapidly since GPT-4 pricing is about 15 times higher than GPT-3.5;
- If you are using GPT-3.5 and not using it frequently, but still find your bill increasing fast, please troubleshoot immediately using these steps:
- Check your API key consumption record on the OpenAI website; if your token is consumed every hour and each time consumes tens of thousands of tokens, your key must have been leaked. Please delete it and regenerate it immediately. **Do not check your balance on random websites.**
- If your password is short, such as 5 characters or fewer, the cost of brute-forcing is very low. It is recommended to search docker logs to confirm whether someone has tried a large number of password combinations. Keyword: got access code
- By following these two methods, you can locate the reason for your token's rapid consumption:
- If the OpenAI consumption record is abnormal but the Docker log has no issues, it means your API key has been leaked;
- If the Docker log shows a large number of got access code brute-force attempts, your password has been cracked.

View File

@@ -12,6 +12,7 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^16.2.0",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7", "emoji-picker-react": "^4.4.7",

View File

@@ -23,6 +23,6 @@
"@/*": ["./*"] "@/*": ["./*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@@ -954,7 +954,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4": "@babel/runtime@^7.12.1", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.21.0" version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
@@ -1027,6 +1027,19 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d"
integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A== integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==
"@hello-pangea/dnd@^16.2.0":
version "16.2.0"
resolved "https://registry.npmmirror.com/@hello-pangea/dnd/-/dnd-16.2.0.tgz#58cbadeb56f8c7a381da696bb7aa3bfbb87876ec"
integrity sha512-inACvMcvvLr34CG0P6+G/3bprVKhwswxjcsFUSJ+fpOGjhvDj9caiA9X3clby0lgJ6/ILIJjyedHZYECB7GAgA==
dependencies:
"@babel/runtime" "^7.19.4"
css-box-model "^1.2.1"
memoize-one "^6.0.0"
raf-schd "^4.0.3"
react-redux "^8.0.4"
redux "^4.2.0"
use-memo-one "^1.1.3"
"@humanwhocodes/config-array@^0.11.8": "@humanwhocodes/config-array@^0.11.8":
version "0.11.8" version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -1333,6 +1346,14 @@
dependencies: dependencies:
"@types/unist" "*" "@types/unist" "*"
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/json5@^0.0.29": "@types/json5@^0.0.29":
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -1408,6 +1429,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@types/use-sync-external-store@^0.0.3":
version "0.0.3"
resolved "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
"@typescript-eslint/parser@^5.42.0": "@typescript-eslint/parser@^5.42.0":
version "5.57.0" version "5.57.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.57.0.tgz#f675bf2cd1a838949fd0de5683834417b757e4fa" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.57.0.tgz#f675bf2cd1a838949fd0de5683834417b757e4fa"
@@ -1912,6 +1938,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
css-box-model@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-select@^4.1.3: css-select@^4.1.3:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
@@ -2885,6 +2918,13 @@ highlight.js@~11.7.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ== integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
human-signals@^4.3.0: human-signals@^4.3.0:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
@@ -3527,6 +3567,11 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
merge-stream@^2.0.0: merge-stream@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -4211,6 +4256,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
raf-schd@^4.0.3:
version "4.0.3"
resolved "https://registry.npmmirror.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
react-dom@^18.2.0: react-dom@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@@ -4219,7 +4269,7 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-is@^16.13.1: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -4250,6 +4300,18 @@ react-markdown@^8.0.5:
unist-util-visit "^4.0.0" unist-util-visit "^4.0.0"
vfile "^5.0.0" vfile "^5.0.0"
react-redux@^8.0.4:
version "8.0.5"
resolved "https://registry.npmmirror.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
integrity sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==
dependencies:
"@babel/runtime" "^7.12.1"
"@types/hoist-non-react-statics" "^3.3.1"
"@types/use-sync-external-store" "^0.0.3"
hoist-non-react-statics "^3.3.2"
react-is "^18.0.0"
use-sync-external-store "^1.0.0"
react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -4264,6 +4326,13 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
redux@^4.2.0:
version "4.2.1"
resolved "https://registry.npmmirror.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
regenerate-unicode-properties@^10.1.0: regenerate-unicode-properties@^10.1.0:
version "10.1.0" version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
@@ -4774,6 +4843,11 @@ tiny-glob@^0.2.9:
globalyzer "0.1.0" globalyzer "0.1.0"
globrex "^0.1.2" globrex "^0.1.2"
tiny-invariant@^1.0.6:
version "1.3.1"
resolved "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
to-fast-properties@^2.0.0: to-fast-properties@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -4979,7 +5053,12 @@ use-debounce@^9.0.3:
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.3.tgz#bac660c19ab7b38662e08608fee23c7ad303f532" resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.3.tgz#bac660c19ab7b38662e08608fee23c7ad303f532"
integrity sha512-FhtlbDtDXILJV7Lix5OZj5yX/fW1tzq+VrvK1fnT2bUrPOGruU9Rw8NCEn+UI9wopfERBEZAOQ8lfeCJPllgnw== integrity sha512-FhtlbDtDXILJV7Lix5OZj5yX/fW1tzq+VrvK1fnT2bUrPOGruU9Rw8NCEn+UI9wopfERBEZAOQ8lfeCJPllgnw==
use-sync-external-store@1.2.0: use-memo-one@^1.1.3:
version "1.1.3"
resolved "https://registry.npmmirror.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==