mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-02 05:13:06 +08:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0d85dae239 | ||
|
7893693706 | ||
|
25ce6af36e | ||
|
c0269254e8 | ||
|
2f2aefd48e | ||
|
c05de45d99 | ||
|
8f66da1128 | ||
|
b5eaa8272b | ||
|
7ee062e1de | ||
|
8915af9b6d | ||
|
ae1ef3215b | ||
|
5d06fa217c | ||
|
50b1f7db12 | ||
|
fb82806956 | ||
|
0658a93962 | ||
|
f33be37070 | ||
|
9278d7f851 | ||
|
35ba898c97 | ||
|
15f8d13d81 | ||
|
946e508e8f | ||
|
35b4125b98 | ||
|
91d8f9d73e | ||
|
c636993989 | ||
|
65a24c16bc | ||
|
15ce114440 | ||
|
1722f75dcb | ||
|
be597a551d | ||
|
56bc945335 | ||
|
fa9ceb5875 | ||
|
0ccf5a16c7 | ||
|
93c666b03d | ||
|
d5c28b506c | ||
|
3318f01aa1 | ||
|
1641019091 | ||
|
3572969866 | ||
|
ad1fe8762b | ||
|
c9c3e32a78 | ||
|
f4c99c9cf7 | ||
|
57447c24c8 |
@@ -27,3 +27,8 @@ HIDE_USER_API_KEY=
|
||||
# Default: Empty
|
||||
# If you do not want users to use GPT-4, set this value to 1.
|
||||
DISABLE_GPT4=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to query balance, set this value to 1.
|
||||
HIDE_BALANCE_QUERY=
|
21
README.md
21
README.md
@@ -19,15 +19,10 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
[网页版](https://chatgpt.nextweb.fun/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||
|
||||
[web-url]: https://chatgpt.nextweb.fun
|
||||
|
||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
||||
|
||||
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
|
||||
|
||||
[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
|
||||
|
||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||
|
||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||
|
||||
[](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)
|
||||
@@ -190,6 +185,16 @@ If you do not want users to input their own API key, set this value to 1.
|
||||
|
||||
If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
### `HIDE_BALANCE_QUERY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do not want users to query balance, set this value to 1.
|
||||
|
||||
## Requirements
|
||||
|
||||
NodeJS >= 18, Docker >= 20
|
||||
|
||||
## Development
|
||||
|
||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||
@@ -240,6 +245,12 @@ docker run -d -p 3000:3000 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
If your proxy needs password, use:
|
||||
|
||||
```shell
|
||||
-e PROXY_URL="http://127.0.0.1:7890 user pass"
|
||||
```
|
||||
|
||||
### Shell
|
||||
|
||||
```shell
|
||||
|
10
README_CN.md
10
README_CN.md
@@ -98,6 +98,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
|
||||
|
||||
### `HIDE_BALANCE_QUERY` (可选)
|
||||
|
||||
如果你不想让用户查询余额,将此环境变量设置为 1 即可。
|
||||
|
||||
## 开发
|
||||
|
||||
点击下方按钮,开始二次开发:
|
||||
@@ -147,6 +151,12 @@ docker run -d -p 3000:3000 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
如果你的本地代理需要账号密码,可以使用:
|
||||
|
||||
```shell
|
||||
-e PROXY_URL="http://127.0.0.1:7890 user password"
|
||||
```
|
||||
|
||||
如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
|
||||
|
||||
### 本地部署
|
||||
|
@@ -96,6 +96,10 @@ Si no desea que los usuarios rellenen la clave de API ellos mismos, establezca e
|
||||
|
||||
Si no desea que los usuarios utilicen GPT-4, establezca esta variable de entorno en 1.
|
||||
|
||||
### `HIDE_BALANCE_QUERY` (Opcional)
|
||||
|
||||
Si no desea que los usuarios consulte el saldo, establezca esta variable de entorno en 1.
|
||||
|
||||
## explotación
|
||||
|
||||
> No se recomienda encarecidamente desarrollar o implementar localmente, debido a algunas razones técnicas, es difícil configurar el agente API de OpenAI localmente, a menos que pueda asegurarse de que puede conectarse directamente al servidor OpenAI.
|
||||
|
@@ -10,6 +10,7 @@ const DANGER_CONFIG = {
|
||||
needCode: serverConfig.needCode,
|
||||
hideUserApiKey: serverConfig.hideUserApiKey,
|
||||
enableGPT4: serverConfig.enableGPT4,
|
||||
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import Locale from "./locales";
|
||||
|
||||
type Command = (param: string) => void;
|
||||
interface Commands {
|
||||
@@ -26,3 +27,45 @@ export function useCommand(commands: Commands = {}) {
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatCommands {
|
||||
new?: Command;
|
||||
newm?: Command;
|
||||
next?: Command;
|
||||
prev?: Command;
|
||||
clear?: Command;
|
||||
del?: Command;
|
||||
}
|
||||
|
||||
export const ChatCommandPrefix = ":";
|
||||
|
||||
export function useChatCommand(commands: ChatCommands = {}) {
|
||||
function extract(userInput: string) {
|
||||
return (
|
||||
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
|
||||
) as keyof ChatCommands;
|
||||
}
|
||||
|
||||
function search(userInput: string) {
|
||||
const input = extract(userInput);
|
||||
const desc = Locale.Chat.Commands;
|
||||
return Object.keys(commands)
|
||||
.filter((c) => c.startsWith(input))
|
||||
.map((c) => ({
|
||||
title: desc[c as keyof ChatCommands],
|
||||
content: ChatCommandPrefix + c,
|
||||
}));
|
||||
}
|
||||
|
||||
function match(userInput: string) {
|
||||
const command = extract(userInput);
|
||||
const matched = typeof commands[command] === "function";
|
||||
|
||||
return {
|
||||
matched,
|
||||
invoke: () => matched && commands[command]!(userInput),
|
||||
};
|
||||
}
|
||||
|
||||
return { match, search };
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.chat-input-action {
|
||||
display: inline-flex;
|
||||
@@ -15,7 +16,6 @@
|
||||
animation: slide-in ease 0.3s;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all ease 0.3s;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
width: var(--icon-width);
|
||||
@@ -202,3 +202,225 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&:last-child {
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.chat-message-container {
|
||||
max-width: var(--message-max-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-avatar {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chat-message-status {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chat-message-item {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
user-select: text;
|
||||
word-break: break-word;
|
||||
border: var(--border-in-light);
|
||||
position: relative;
|
||||
transition: all ease 0.3s;
|
||||
min-width: 0;
|
||||
|
||||
&:hover {
|
||||
min-width: 330px;
|
||||
|
||||
.chat-message-actions {
|
||||
height: 40px;
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
|
||||
.chat-message-action-date {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
transition: all ease 0.3s;
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.chat-message-action-date {
|
||||
color: var(--black);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container > .chat-message-item {
|
||||
background-color: var(--second);
|
||||
|
||||
&:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding-top: 10px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
border-top: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
@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 {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
font-family: inherit;
|
||||
padding: 10px 90px 10px 14px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-input:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.chat-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@ import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react";
|
||||
|
||||
import SendWhiteIcon from "../icons/send-white.svg";
|
||||
@@ -21,12 +21,15 @@ import MinIcon from "../icons/min.svg";
|
||||
import ResetIcon from "../icons/reload.svg";
|
||||
import BreakIcon from "../icons/break.svg";
|
||||
import SettingsIcon from "../icons/chat-settings.svg";
|
||||
import DeleteIcon from "../icons/clear.svg";
|
||||
import PinIcon from "../icons/pin.svg";
|
||||
|
||||
import LightIcon from "../icons/light.svg";
|
||||
import DarkIcon from "../icons/dark.svg";
|
||||
import AutoIcon from "../icons/auto.svg";
|
||||
import BottomIcon from "../icons/bottom.svg";
|
||||
import StopIcon from "../icons/pause.svg";
|
||||
import RobotIcon from "../icons/robot.svg";
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
@@ -38,6 +41,7 @@ import {
|
||||
Theme,
|
||||
useAppConfig,
|
||||
DEFAULT_TOPIC,
|
||||
ALL_MODELS,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@@ -55,16 +59,15 @@ import { Prompt, usePromptStore } from "../store/prompt";
|
||||
import Locale from "../locales";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./home.module.scss";
|
||||
import chatStyle from "./chat.module.scss";
|
||||
import styles from "./chat.module.scss";
|
||||
|
||||
import { ListItem, Modal } from "./ui-lib";
|
||||
import { ListItem, Modal, showToast } from "./ui-lib";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { MaskAvatar, MaskConfig } from "./mask";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { useCommand } from "../command";
|
||||
import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
@@ -146,15 +149,15 @@ function PromptToast(props: {
|
||||
const context = session.mask.context;
|
||||
|
||||
return (
|
||||
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
||||
<div className={styles["prompt-toast"]} key="prompt-toast">
|
||||
{props.showToast && (
|
||||
<div
|
||||
className={chatStyle["prompt-toast-inner"] + " clickable"}
|
||||
className={styles["prompt-toast-inner"] + " clickable"}
|
||||
role="button"
|
||||
onClick={() => props.setShowModal(true)}
|
||||
>
|
||||
<BrainIcon />
|
||||
<span className={chatStyle["prompt-toast-content"]}>
|
||||
<span className={styles["prompt-toast-content"]}>
|
||||
{Locale.Context.Toast(context.length)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -206,8 +209,7 @@ export function PromptHints(props: {
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (noPrompts) return;
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
// arrow up / down to select prompt
|
||||
@@ -269,17 +271,15 @@ function ClearContextDivider() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={chatStyle["clear-context"]}
|
||||
className={styles["clear-context"]}
|
||||
onClick={() =>
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.clearContextIndex = undefined),
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={chatStyle["clear-context-tips"]}>
|
||||
{Locale.Context.Clear}
|
||||
</div>
|
||||
<div className={chatStyle["clear-context-revert-btn"]}>
|
||||
<div className={styles["clear-context-tips"]}>{Locale.Context.Clear}</div>
|
||||
<div className={styles["clear-context-revert-btn"]}>
|
||||
{Locale.Context.Revert}
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,7 +315,7 @@ function ChatAction(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||
className={`${styles["chat-input-action"]} clickable`}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
setTimeout(updateWidth, 1);
|
||||
@@ -327,10 +327,10 @@ function ChatAction(props: {
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div ref={iconRef} className={chatStyle["icon"]}>
|
||||
<div ref={iconRef} className={styles["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className={chatStyle["text"]} ref={textRef}>
|
||||
<div className={styles["text"]} ref={textRef}>
|
||||
{props.text}
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,15 +341,15 @@ function useScrollToBottom() {
|
||||
// for auto-scroll
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollToBottom = () => {
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom) {
|
||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||
requestAnimationFrame(() => dom.scrollTo(0, dom.scrollHeight));
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// auto scroll
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
autoScroll && scrollToBottom();
|
||||
});
|
||||
|
||||
@@ -385,8 +385,21 @@ export function ChatActions(props: {
|
||||
const couldStop = ChatControllerPool.hasPending();
|
||||
const stopAll = () => ChatControllerPool.stopAll();
|
||||
|
||||
// switch model
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
function nextModel() {
|
||||
const models = ALL_MODELS.filter((m) => m.available).map((m) => m.name);
|
||||
const modelIndex = models.indexOf(currentModel);
|
||||
const nextIndex = (modelIndex + 1) % models.length;
|
||||
const nextModel = models[nextIndex];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.model = nextModel;
|
||||
session.mask.syncGlobalConfig = false;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={chatStyle["chat-input-actions"]}>
|
||||
<div className={styles["chat-input-actions"]}>
|
||||
{couldStop && (
|
||||
<ChatAction
|
||||
onClick={stopAll}
|
||||
@@ -453,6 +466,12 @@ export function ChatActions(props: {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
onClick={nextModel}
|
||||
text={currentModel}
|
||||
icon={<RobotIcon />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -480,7 +499,7 @@ export function Chat() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 100;
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 10;
|
||||
setHitBottom(isTouchBottom);
|
||||
};
|
||||
|
||||
@@ -489,16 +508,19 @@ export function Chat() {
|
||||
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
||||
const onSearch = useDebouncedCallback(
|
||||
(text: string) => {
|
||||
setPromptHints(promptStore.search(text));
|
||||
const matchedPrompts = promptStore.search(text);
|
||||
setPromptHints(matchedPrompts);
|
||||
},
|
||||
100,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
const onPromptSelect = (prompt: Prompt) => {
|
||||
setTimeout(() => {
|
||||
setPromptHints([]);
|
||||
setUserInput(prompt.content);
|
||||
inputRef.current?.focus();
|
||||
setTimeout(() => setUserInput(prompt.content), 60);
|
||||
}, 30);
|
||||
};
|
||||
|
||||
// auto grow input
|
||||
@@ -522,6 +544,19 @@ export function Chat() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(measure, [userInput]);
|
||||
|
||||
// chat commands shortcuts
|
||||
const chatCommands = useChatCommand({
|
||||
new: () => chatStore.newSession(),
|
||||
newm: () => navigate(Path.NewChat),
|
||||
prev: () => chatStore.nextSession(-1),
|
||||
next: () => chatStore.nextSession(1),
|
||||
clear: () =>
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.clearContextIndex = session.messages.length),
|
||||
),
|
||||
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
|
||||
});
|
||||
|
||||
// only search prompts when user input is short
|
||||
const SEARCH_TEXT_LIMIT = 30;
|
||||
const onInput = (text: string) => {
|
||||
@@ -531,6 +566,8 @@ export function Chat() {
|
||||
// clear search results
|
||||
if (n === 0) {
|
||||
setPromptHints([]);
|
||||
} else if (text.startsWith(ChatCommandPrefix)) {
|
||||
setPromptHints(chatCommands.search(text));
|
||||
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||
// check if need to trigger auto completion
|
||||
if (text.startsWith("/")) {
|
||||
@@ -542,6 +579,13 @@ export function Chat() {
|
||||
|
||||
const doSubmit = (userInput: string) => {
|
||||
if (userInput.trim() === "") return;
|
||||
const matchCommand = chatCommands.match(userInput);
|
||||
if (matchCommand.matched) {
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
matchCommand.invoke();
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||
@@ -605,6 +649,10 @@ export function Chat() {
|
||||
const onRightClick = (e: any, message: ChatMessage) => {
|
||||
// copy to clipboard
|
||||
if (selectOrCopy(e.currentTarget, message.content)) {
|
||||
if (userInput.length === 0) {
|
||||
setUserInput(message.content);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
@@ -649,6 +697,24 @@ export function Chat() {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const onPinMessage = (botMessage: ChatMessage) => {
|
||||
if (!botMessage.id) return;
|
||||
const userMessageIndex = findLastUserIndex(botMessage.id);
|
||||
if (!userMessageIndex) return;
|
||||
|
||||
const userMessage = session.messages[userMessageIndex];
|
||||
chatStore.updateCurrentSession((session) =>
|
||||
session.mask.context.push(userMessage, botMessage),
|
||||
);
|
||||
|
||||
showToast(Locale.Chat.Actions.PinToastContent, {
|
||||
text: Locale.Chat.Actions.PinToastAction,
|
||||
onClick: () => {
|
||||
setShowPromptModal(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const context: RenderMessage[] = session.mask.hideContext
|
||||
? []
|
||||
: session.mask.context.slice();
|
||||
@@ -831,40 +897,6 @@ export function Chat() {
|
||||
</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
{showActions && (
|
||||
<div className={styles["chat-message-top-actions"]}>
|
||||
{message.streaming ? (
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onUserStop(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Stop}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Delete}
|
||||
</div>
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => onResend(message.id ?? i)}
|
||||
>
|
||||
{Locale.Chat.Actions.Retry}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={styles["chat-message-top-action"]}
|
||||
onClick={() => copyToClipboard(message.content)}
|
||||
>
|
||||
{Locale.Chat.Actions.Copy}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Markdown
|
||||
content={message.content}
|
||||
loading={
|
||||
@@ -880,9 +912,50 @@ export function Chat() {
|
||||
parentRef={scrollRef}
|
||||
defaultShow={i >= messages.length - 10}
|
||||
/>
|
||||
</div>
|
||||
{!isUser && !message.preview && (
|
||||
|
||||
{showActions && (
|
||||
<div className={styles["chat-message-actions"]}>
|
||||
<div
|
||||
className={styles["chat-input-actions"]}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
{message.streaming ? (
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Stop}
|
||||
icon={<StopIcon />}
|
||||
onClick={() => onUserStop(message.id ?? i)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Delete}
|
||||
icon={<DeleteIcon />}
|
||||
onClick={() => onDelete(message.id ?? i)}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Retry}
|
||||
icon={<ResetIcon />}
|
||||
onClick={() => onResend(message.id ?? i)}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Pin}
|
||||
icon={<PinIcon />}
|
||||
onClick={() => onPinMessage(message)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ChatAction
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
icon={<CopyIcon />}
|
||||
onClick={() => copyToClipboard(message.content)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles["chat-message-action-date"]}>
|
||||
{message.date.toLocaleString()}
|
||||
</div>
|
||||
@@ -890,6 +963,7 @@ export function Chat() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowClearContextDivider && <ClearContextDivider />}
|
||||
</>
|
||||
);
|
||||
@@ -927,6 +1001,9 @@ export function Chat() {
|
||||
onBlur={() => setAutoScroll(false)}
|
||||
rows={inputRows}
|
||||
autoFocus={autoFocus}
|
||||
style={{
|
||||
fontSize: config.fontSize,
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
|
@@ -185,7 +185,7 @@
|
||||
|
||||
.chat-item-delete {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0;
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
transform: translateX(-10px);
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete:hover {
|
||||
@@ -283,15 +283,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item-delete {
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.sidebar-tail {
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
@@ -322,243 +313,6 @@
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&:last-child {
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.chat-message-container {
|
||||
max-width: var(--message-max-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&:hover {
|
||||
.chat-message-top-actions {
|
||||
opacity: 1;
|
||||
transform: translateX(10px);
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-message-avatar {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chat-message-status {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chat-message-item {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
user-select: text;
|
||||
word-break: break-word;
|
||||
border: var(--border-in-light);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-message-top-actions {
|
||||
min-width: 120px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: -26px;
|
||||
left: 30px;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-message-top-action {
|
||||
opacity: 0.5;
|
||||
color: var(--black);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user > .chat-message-container > .chat-message-item {
|
||||
background-color: var(--second);
|
||||
}
|
||||
|
||||
.chat-message-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-message-action-date {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.chat-input-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding-top: 10px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-top: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
@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 {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
font-family: inherit;
|
||||
padding: 10px 90px 10px 14px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-input:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.chat-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -567,3 +321,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rtl-screen {
|
||||
direction: rtl;
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ import dynamic from "next/dynamic";
|
||||
import { Path, SlotID } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
import { getLang } from "../locales";
|
||||
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
@@ -124,7 +126,7 @@ function Screen() {
|
||||
config.tightBorder && !isMobileScreen
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`
|
||||
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`
|
||||
}
|
||||
>
|
||||
{isAuth ? (
|
||||
|
@@ -195,6 +195,7 @@ export function Markdown(
|
||||
fontSize: `${props.fontSize ?? 14}px`,
|
||||
height: getSize(renderedHeight.current),
|
||||
width: getSize(renderedWidth.current),
|
||||
direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr",
|
||||
}}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
|
@@ -2,7 +2,7 @@ import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { InputRange } from "./input-range";
|
||||
import { List, ListItem, Select } from "./ui-lib";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
|
||||
export function ModelConfigList(props: {
|
||||
modelConfig: ModelConfig;
|
||||
@@ -109,6 +109,21 @@ export function ModelConfigList(props: {
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.InputTemplate.Title}
|
||||
subTitle={Locale.Settings.InputTemplate.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={props.modelConfig.template}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.template = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.HistoryCount.Title}
|
||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||
|
@@ -522,6 +522,7 @@ export function Settings() {
|
||||
</ListItem>
|
||||
) : null}
|
||||
|
||||
{!accessStore.hideBalanceQuery ? (
|
||||
<ListItem
|
||||
title={Locale.Settings.Usage.Title}
|
||||
subTitle={
|
||||
@@ -545,6 +546,7 @@ export function Settings() {
|
||||
/>
|
||||
)}
|
||||
</ListItem>
|
||||
) : null}
|
||||
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<ListItem
|
||||
|
@@ -38,13 +38,10 @@ function useHotKey() {
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
const n = chatStore.sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = chatStore.currentSessionIndex;
|
||||
if (e.key === "ArrowUp") {
|
||||
chatStore.selectSession(limit(i - 1));
|
||||
chatStore.nextSession(-1);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
chatStore.selectSession(limit(i + 1));
|
||||
chatStore.nextSession(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -12,6 +12,7 @@ declare global {
|
||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||
BUILD_MODE?: "standalone" | "export";
|
||||
BUILD_APP?: string; // is building desktop app
|
||||
HIDE_BALANCE_QUERY?: string; // allow user to query balance or not
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,5 +47,6 @@ export const getServerSideConfig = () => {
|
||||
isVercel: !!process.env.VERCEL,
|
||||
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
||||
enableGPT4: !process.env.DISABLE_GPT4,
|
||||
hideBalanceQuery: !!process.env.HIDE_BALANCE_QUERY,
|
||||
};
|
||||
};
|
||||
|
@@ -52,3 +52,5 @@ export const OpenaiPath = {
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
SubsPath: "dashboard/billing/subscription",
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
|
1
app/icons/pin.svg
Normal file
1
app/icons/pin.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.8 KiB |
1
app/icons/robot.svg
Normal file
1
app/icons/robot.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
296
app/locales/ar.ts
Normal file
296
app/locales/ar.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
|
||||
const ar: PartialLocaleType = {
|
||||
WIP: "قريبًا...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"غير مصرح بالوصول، يرجى إدخال رمز الوصول [auth](/#/auth) في صفحة المصادقة.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "تحتاج إلى رمز الوصول",
|
||||
Tips: "يرجى إدخال رمز الوصول أدناه",
|
||||
Input: "رمز الوصول",
|
||||
Confirm: "تأكيد",
|
||||
Later: "لاحقًا",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} رسائل`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => ` ${count} رسائل مع ChatGPT`,
|
||||
Actions: {
|
||||
ChatList: "الانتقال إلى قائمة الدردشة",
|
||||
CompressedHistory: "ملخص ضغط ذاكرة التاريخ",
|
||||
Export: "تصدير جميع الرسائل كـ Markdown",
|
||||
Copy: "نسخ",
|
||||
Stop: "توقف",
|
||||
Retry: "إعادة المحاولة",
|
||||
Delete: "حذف",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "توقف",
|
||||
ToBottom: "إلى آخر",
|
||||
Theme: {
|
||||
auto: "تلقائي",
|
||||
light: "نمط فاتح",
|
||||
dark: "نمط داكن",
|
||||
},
|
||||
Prompt: "الاقتراحات",
|
||||
Masks: "الأقنعة",
|
||||
Clear: "مسح السياق",
|
||||
Settings: "الإعدادات",
|
||||
},
|
||||
Rename: "إعادة تسمية الدردشة",
|
||||
Typing: "كتابة...",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = ` اضغط على ${submitKey} للإرسال`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += "، Shift + Enter للإنشاء";
|
||||
}
|
||||
return inputHints + "، / للبحث في الاقتراحات";
|
||||
},
|
||||
Send: "إرسال",
|
||||
Config: {
|
||||
Reset: "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
SaveAs: "حفظ كأقنعة",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "تصدير الرسائل",
|
||||
Copy: "نسخ الكل",
|
||||
Download: "تنزيل",
|
||||
MessageFromYou: "رسالة منك",
|
||||
MessageFromChatGPT: "رسالة من ChatGPT",
|
||||
Share: "مشاركة على ShareGPT",
|
||||
Format: {
|
||||
Title: "صيغة التصدير",
|
||||
SubTitle: "Markdown أو صورة PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "تضمين السياق",
|
||||
SubTitle: "تصدير اقتراحات السياق في الأقنعة أم لا",
|
||||
},
|
||||
Steps: {
|
||||
Select: "تحديد",
|
||||
Preview: "معاينة",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "بحث",
|
||||
All: "تحديد الكل",
|
||||
Latest: "تحديد أحدث",
|
||||
Clear: "مسح",
|
||||
},
|
||||
Memory: {
|
||||
Title: "اقتراحات الذاكرة",
|
||||
EmptyContent: "لا شيء حتى الآن.",
|
||||
Send: "إرسال الذاكرة",
|
||||
Copy: "نسخ الذاكرة",
|
||||
Reset: "إعادة التعيين",
|
||||
ResetConfirm:
|
||||
"سيؤدي إعادة التعيين إلى مسح سجل المحادثة الحالي والذاكرة التاريخية. هل أنت متأكد أنك تريد الاستمرار؟",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "دردشة جديدة",
|
||||
DeleteChat: "هل تريد تأكيد حذف المحادثة المحددة؟",
|
||||
DeleteToast: "تم حذف الدردشة",
|
||||
Revert: "التراجع",
|
||||
},
|
||||
Settings: {
|
||||
Title: "الإعدادات",
|
||||
SubTitle: "جميع الإعدادات",
|
||||
Actions: {
|
||||
ClearAll: "مسح جميع البيانات",
|
||||
ResetAll: "إعادة تعيين جميع الإعدادات",
|
||||
Close: "إغلاق",
|
||||
ConfirmResetAll: "هل أنت متأكد من رغبتك في إعادة تعيين جميع الإعدادات؟",
|
||||
ConfirmClearAll: "هل أنت متأكد من رغبتك في مسح جميع البيانات؟",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language"
|
||||
All: "كل اللغات",
|
||||
},
|
||||
Avatar: "الصورة الرمزية",
|
||||
FontSize: {
|
||||
Title: "حجم الخط",
|
||||
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "نموذج الإدخال",
|
||||
SubTitle: "سيتم ملء أحدث رسالة في هذا النموذج",
|
||||
},
|
||||
Update: {
|
||||
Version: (x: string) => ` الإصدار: ${x}`,
|
||||
IsLatest: "أحدث إصدار",
|
||||
CheckUpdate: "التحقق من التحديث",
|
||||
IsChecking: "جارٍ التحقق من التحديث...",
|
||||
FoundUpdate: (x: string) => ` تم العثور على إصدار جديد: ${x}`,
|
||||
GoToUpdate: "التحديث",
|
||||
},
|
||||
SendKey: "مفتاح الإرسال",
|
||||
Theme: "السمة",
|
||||
TightBorder: "حدود ضيقة",
|
||||
SendPreviewBubble: {
|
||||
Title: "عرض معاينة الـ Send",
|
||||
SubTitle: "معاينة Markdown في فقاعة",
|
||||
},
|
||||
Mask: {
|
||||
Title: "شاشة تظهر الأقنعة",
|
||||
SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة",
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "تعطيل الاكتمال التلقائي",
|
||||
SubTitle: "اكتب / لتشغيل الاكتمال التلقائي",
|
||||
},
|
||||
List: "قائمة الاقتراحات",
|
||||
ListCount: (builtin: number, custom: number) => `
|
||||
${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخدم`,
|
||||
Edit: "تعديل",
|
||||
Modal: {
|
||||
Title: "قائمة الاقتراحات",
|
||||
Add: "إضافة واحدة",
|
||||
Search: "البحث في الاقتراحات",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "تحرير الاقتراح",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "عدد الرسائل المرفقة",
|
||||
SubTitle: "عدد الرسائل المرسلة المرفقة في كل طلب",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "حد الضغط للتاريخ",
|
||||
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
|
||||
},
|
||||
Token: {
|
||||
Title: "مفتاح API",
|
||||
SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
|
||||
Placeholder: "مفتاح OpenAI API",
|
||||
},
|
||||
Usage: {
|
||||
Title: "رصيد الحساب",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `تم استخدام $${used} من هذا الشهر، الاشتراك ${total}`;
|
||||
},
|
||||
IsChecking: "جارٍ التحقق...",
|
||||
Check: "التحقق",
|
||||
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "رمز الوصول",
|
||||
SubTitle: "تم تمكين التحكم في الوصول",
|
||||
Placeholder: "رمز الوصول المطلوب",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "نقطة النهاية",
|
||||
SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
|
||||
},
|
||||
Model: "النموذج",
|
||||
Temperature: {
|
||||
Title: "الحرارة",
|
||||
SubTitle: "قيمة أكبر تجعل الإخراج أكثر عشوائية",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "الحد الأقصى للرموز",
|
||||
SubTitle: "الحد الأقصى لعدد الرموز المدخلة والرموز المُنشأة",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "تأثير الوجود",
|
||||
SubTitle: "قيمة أكبر تزيد من احتمالية التحدث عن مواضيع جديدة",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "تأثير التكرار",
|
||||
SubTitle: "قيمة أكبر تقلل من احتمالية تكرار نفس السطر",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "محادثة جديدة",
|
||||
BotHello: "مرحبًا! كيف يمكنني مساعدتك اليوم؟",
|
||||
Error: "حدث خطأ ما، يرجى المحاولة مرة أخرى في وقت لاحق.",
|
||||
Prompt: {
|
||||
History: (content: string) => "هذا ملخص لسجل الدردشة كمراجعة: " + content,
|
||||
Topic:
|
||||
"يرجى إنشاء عنوان يتكون من أربع إلى خمس كلمات يلخص محادثتنا دون أي مقدمة أو ترقيم أو علامات ترقيم أو نقاط أو رموز إضافية. قم بإزالة علامات التنصيص المحيطة.",
|
||||
Summarize:
|
||||
"قم بتلخيص النقاش بشكل موجز في 200 كلمة أو أقل لاستخدامه كاقتراح للسياق في المستقبل.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "تم النسخ إلى الحافظة",
|
||||
Failed: "فشلت عملية النسخ، يرجى منح الإذن للوصول إلى الحافظة",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `مع ${x} اقتراحًا ذا سياق`,
|
||||
Edit: "الاقتراحات السياقية والذاكرة",
|
||||
Add: "إضافة اقتراح",
|
||||
Clear: "مسح السياق",
|
||||
Revert: "التراجع",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "المكوّن الإضافي",
|
||||
},
|
||||
Mask: {
|
||||
Name: "الأقنعة",
|
||||
Page: {
|
||||
Title: "قالب الاقتراح",
|
||||
SubTitle: (count: number) => `${count} قوالب الاقتراح`,
|
||||
Search: "البحث في القوالب",
|
||||
Create: "إنشاء",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} اقتراحات`,
|
||||
Chat: "الدردشة",
|
||||
View: "عرض",
|
||||
Edit: "تعديل",
|
||||
Delete: "حذف",
|
||||
DeleteConfirm: "تأكيد الحذف؟",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) => `
|
||||
تعديل قالب الاقتراح ${readonly ? "(للقراءة فقط)" : ""}`,
|
||||
Download: "تنزيل",
|
||||
Clone: "استنساخ",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "صورة الروبوت",
|
||||
Name: "اسم الروبوت",
|
||||
Sync: {
|
||||
Title: "استخدام الإعدادات العامة",
|
||||
SubTitle: "استخدام الإعدادات العامة في هذه الدردشة",
|
||||
Confirm: "تأكيد الاستبدال بالإعدادات المخصصة بالإعدادات العامة؟",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "إخفاء اقتراحات السياق",
|
||||
SubTitle: "عدم عرض اقتراحات السياق في الدردشة",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "العودة",
|
||||
Skip: "ابدأ فقط",
|
||||
Title: "اختيار قناع",
|
||||
SubTitle: "دردشة مع الروح وراء القناع",
|
||||
More: "المزيد",
|
||||
NotShow: "عدم العرض مرة أخرى",
|
||||
ConfirmNoShow: "تأكيد تعطيله؟ يمكنك تمكينه في الإعدادات لاحقًا.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "تأكيد",
|
||||
Cancel: "إلغاء",
|
||||
Close: "إغلاق",
|
||||
Create: "إنشاء",
|
||||
Edit: "تعديل",
|
||||
},
|
||||
Exporter: {
|
||||
Model: "النموذج",
|
||||
Messages: "الرسائل",
|
||||
Topic: "الموضوع",
|
||||
Time: "الوقت",
|
||||
},
|
||||
};
|
||||
|
||||
export default ar;
|
@@ -25,8 +25,19 @@ const cn = {
|
||||
Copy: "复制",
|
||||
Stop: "停止",
|
||||
Retry: "重试",
|
||||
Pin: "固定",
|
||||
PinToastContent: "已将 2 条对话固定至预设提示词",
|
||||
PinToastAction: "查看",
|
||||
Delete: "删除",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
newm: "从面具新建聊天",
|
||||
next: "下一个聊天",
|
||||
prev: "上一个聊天",
|
||||
clear: "清除上下文",
|
||||
del: "删除聊天",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "停止响应",
|
||||
ToBottom: "滚到最新",
|
||||
@@ -47,7 +58,7 @@ const cn = {
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ",Shift + Enter 换行";
|
||||
}
|
||||
return inputHints + ",/ 触发补全";
|
||||
return inputHints + ",/ 触发补全,: 触发命令";
|
||||
},
|
||||
Send: "发送",
|
||||
Config: {
|
||||
@@ -115,6 +126,11 @@ const cn = {
|
||||
SubTitle: "聊天内容的字体大小",
|
||||
},
|
||||
|
||||
InputTemplate: {
|
||||
Title: "用户输入预处理",
|
||||
SubTitle: "用户最新的一条消息会填充到此模板",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `当前版本:${x}`,
|
||||
IsLatest: "已是最新版本",
|
||||
|
@@ -26,8 +26,19 @@ const en: LocaleType = {
|
||||
Copy: "Copy",
|
||||
Stop: "Stop",
|
||||
Retry: "Retry",
|
||||
Pin: "Pin",
|
||||
PinToastContent: "Pinned 2 messages to contextual prompts",
|
||||
PinToastAction: "View",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Commands: {
|
||||
new: "Start a new chat",
|
||||
newm: "Start a new chat with mask",
|
||||
next: "Next Chat",
|
||||
prev: "Previous Chat",
|
||||
clear: "Clear Context",
|
||||
del: "Delete Chat",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Stop",
|
||||
ToBottom: "To Latest",
|
||||
@@ -48,7 +59,7 @@ const en: LocaleType = {
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter to wrap";
|
||||
}
|
||||
return inputHints + ", / to search prompts";
|
||||
return inputHints + ", / to search prompts, : to use commands";
|
||||
},
|
||||
Send: "Send",
|
||||
Config: {
|
||||
@@ -116,6 +127,12 @@ const en: LocaleType = {
|
||||
Title: "Font Size",
|
||||
SubTitle: "Adjust font size of chat content",
|
||||
},
|
||||
|
||||
InputTemplate: {
|
||||
Title: "Input Template",
|
||||
SubTitle: "Newest message will be filled to this template",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version: ${x}`,
|
||||
IsLatest: "Latest version",
|
||||
|
@@ -12,6 +12,7 @@ import ru from "./ru";
|
||||
import no from "./no";
|
||||
import cs from "./cs";
|
||||
import ko from "./ko";
|
||||
import ar from "./ar";
|
||||
import { merge } from "../utils/merge";
|
||||
|
||||
import type { LocaleType } from "./cn";
|
||||
@@ -32,6 +33,7 @@ const ALL_LANGS = {
|
||||
ru,
|
||||
cs,
|
||||
no,
|
||||
ar,
|
||||
};
|
||||
|
||||
export type Lang = keyof typeof ALL_LANGS;
|
||||
@@ -53,6 +55,7 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
no: "Nynorsk",
|
||||
ar: "العربية",
|
||||
};
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
|
@@ -9,7 +9,7 @@ export const BUILTIN_MASK_ID = 100000;
|
||||
|
||||
export const BUILTIN_MASK_STORE = {
|
||||
buildinId: BUILTIN_MASK_ID,
|
||||
masks: {} as Record<number, Mask>,
|
||||
masks: {} as Record<number, BuiltinMask>,
|
||||
get(id?: number) {
|
||||
if (!id) return undefined;
|
||||
return this.masks[id] as Mask | undefined;
|
||||
@@ -21,6 +21,6 @@ export const BUILTIN_MASK_STORE = {
|
||||
},
|
||||
};
|
||||
|
||||
export const BUILTIN_MASKS: Mask[] = [...CN_MASKS, ...EN_MASKS].map((m) =>
|
||||
BUILTIN_MASK_STORE.add(m),
|
||||
export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...EN_MASKS].map(
|
||||
(m) => BUILTIN_MASK_STORE.add(m),
|
||||
);
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { ModelConfig } from "../store";
|
||||
import { type Mask } from "../store/mask";
|
||||
|
||||
export type BuiltinMask = Omit<Mask, "id"> & {
|
||||
builtin: true;
|
||||
export type BuiltinMask = Omit<Mask, "id" | "modelConfig"> & {
|
||||
builtin: Boolean;
|
||||
modelConfig: Partial<ModelConfig>;
|
||||
};
|
||||
|
@@ -13,6 +13,7 @@ export interface AccessControlStore {
|
||||
needCode: boolean;
|
||||
hideUserApiKey: boolean;
|
||||
openaiUrl: string;
|
||||
hideBalanceQuery: boolean;
|
||||
|
||||
updateToken: (_: string) => void;
|
||||
updateCode: (_: string) => void;
|
||||
@@ -36,6 +37,7 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
hideBalanceQuery: false,
|
||||
|
||||
enabledAccessControl() {
|
||||
get().fetch();
|
||||
|
@@ -3,11 +3,11 @@ import { persist } from "zustand/middleware";
|
||||
|
||||
import { trimTopic } from "../utils";
|
||||
|
||||
import Locale from "../locales";
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import { ModelType } from "./config";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import { StoreKey } from "../constant";
|
||||
import { DEFAULT_INPUT_TEMPLATE, StoreKey } from "../constant";
|
||||
import { api, RequestMessage } from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { prettyObject } from "../utils/format";
|
||||
@@ -85,6 +85,7 @@ interface ChatStore {
|
||||
newSession: (mask?: Mask) => void;
|
||||
deleteSession: (index: number) => void;
|
||||
currentSession: () => ChatSession;
|
||||
nextSession: (delta: number) => void;
|
||||
onNewMessage: (message: ChatMessage) => void;
|
||||
onUserInput: (content: string) => Promise<void>;
|
||||
summarizeSession: () => void;
|
||||
@@ -106,6 +107,29 @@ function countMessages(msgs: ChatMessage[]) {
|
||||
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
|
||||
}
|
||||
|
||||
function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
const vars = {
|
||||
model: modelConfig.model,
|
||||
time: new Date().toLocaleString(),
|
||||
lang: getLang(),
|
||||
input: input,
|
||||
};
|
||||
|
||||
let output = modelConfig.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||
|
||||
// must contains {{input}}
|
||||
const inputVar = "{{input}}";
|
||||
if (!output.includes(inputVar)) {
|
||||
output += "\n" + inputVar;
|
||||
}
|
||||
|
||||
Object.entries(vars).forEach(([name, value]) => {
|
||||
output = output.replaceAll(`{{${name}}}`, value);
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -158,7 +182,16 @@ export const useChatStore = create<ChatStore>()(
|
||||
session.id = get().globalId;
|
||||
|
||||
if (mask) {
|
||||
session.mask = { ...mask };
|
||||
const config = useAppConfig.getState();
|
||||
const globalModelConfig = config.modelConfig;
|
||||
|
||||
session.mask = {
|
||||
...mask,
|
||||
modelConfig: {
|
||||
...globalModelConfig,
|
||||
...mask.modelConfig,
|
||||
},
|
||||
};
|
||||
session.topic = mask.name;
|
||||
}
|
||||
|
||||
@@ -168,6 +201,13 @@ export const useChatStore = create<ChatStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
nextSession(delta) {
|
||||
const n = get().sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = get().currentSessionIndex;
|
||||
get().selectSession(limit(i + delta));
|
||||
},
|
||||
|
||||
deleteSession(index) {
|
||||
const deletingLastSession = get().sessions.length === 1;
|
||||
const deletedSession = get().sessions.at(index);
|
||||
@@ -238,9 +278,12 @@ export const useChatStore = create<ChatStore>()(
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
|
||||
const userContent = fillTemplateWith(content, modelConfig);
|
||||
console.log("[User Input] fill with template: ", userContent);
|
||||
|
||||
const userMessage: ChatMessage = createMessage({
|
||||
role: "user",
|
||||
content,
|
||||
content: userContent,
|
||||
});
|
||||
|
||||
const botMessage: ChatMessage = createMessage({
|
||||
@@ -250,31 +293,22 @@ export const useChatStore = create<ChatStore>()(
|
||||
model: modelConfig.model,
|
||||
});
|
||||
|
||||
const systemInfo = createMessage({
|
||||
role: "system",
|
||||
content: `IMPORTANT: You are a virtual assistant powered by the ${
|
||||
modelConfig.model
|
||||
} model, now time is ${new Date().toLocaleString()}}`,
|
||||
id: botMessage.id! + 1,
|
||||
});
|
||||
|
||||
// get recent messages
|
||||
const systemMessages = [];
|
||||
// if user define a mask with context prompts, wont send system info
|
||||
if (session.mask.context.length === 0) {
|
||||
systemMessages.push(systemInfo);
|
||||
}
|
||||
|
||||
const recentMessages = get().getMessagesWithMemory();
|
||||
const sendMessages = systemMessages.concat(
|
||||
recentMessages.concat(userMessage),
|
||||
);
|
||||
const sendMessages = recentMessages.concat(userMessage);
|
||||
const sessionIndex = get().currentSessionIndex;
|
||||
const messageIndex = get().currentSession().messages.length + 1;
|
||||
|
||||
// save user's and bot's message
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat([userMessage, botMessage]);
|
||||
const savedUserMessage = {
|
||||
...userMessage,
|
||||
content,
|
||||
};
|
||||
session.messages = session.messages.concat([
|
||||
savedUserMessage,
|
||||
botMessage,
|
||||
]);
|
||||
});
|
||||
|
||||
// make request
|
||||
@@ -350,55 +384,61 @@ export const useChatStore = create<ChatStore>()(
|
||||
getMessagesWithMemory() {
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
const clearContextIndex = session.clearContextIndex ?? 0;
|
||||
const messages = session.messages.slice();
|
||||
const totalMessageCount = session.messages.length;
|
||||
|
||||
// wont send cleared context messages
|
||||
const clearedContextMessages = session.messages.slice(
|
||||
session.clearContextIndex ?? 0,
|
||||
);
|
||||
const messages = clearedContextMessages.filter((msg) => !msg.isError);
|
||||
const n = messages.length;
|
||||
|
||||
const context = session.mask.context.slice();
|
||||
// in-context prompts
|
||||
const contextPrompts = session.mask.context.slice();
|
||||
|
||||
// long term memory
|
||||
if (
|
||||
const shouldSendLongTermMemory =
|
||||
modelConfig.sendMemory &&
|
||||
session.memoryPrompt &&
|
||||
session.memoryPrompt.length > 0
|
||||
) {
|
||||
const memoryPrompt = get().getMemoryPrompt();
|
||||
context.push(memoryPrompt);
|
||||
}
|
||||
session.memoryPrompt.length > 0 &&
|
||||
session.lastSummarizeIndex <= clearContextIndex;
|
||||
const longTermMemoryPrompts = shouldSendLongTermMemory
|
||||
? [get().getMemoryPrompt()]
|
||||
: [];
|
||||
const longTermMemoryStartIndex = session.lastSummarizeIndex;
|
||||
|
||||
// get short term and unmemorized long term memory
|
||||
const shortTermMemoryMessageIndex = Math.max(
|
||||
// short term memory
|
||||
const shortTermMemoryStartIndex = Math.max(
|
||||
0,
|
||||
n - modelConfig.historyMessageCount,
|
||||
totalMessageCount - modelConfig.historyMessageCount,
|
||||
);
|
||||
const longTermMemoryMessageIndex = session.lastSummarizeIndex;
|
||||
|
||||
// try to concat history messages
|
||||
const memoryStartIndex = Math.min(
|
||||
shortTermMemoryMessageIndex,
|
||||
longTermMemoryMessageIndex,
|
||||
);
|
||||
const threshold = modelConfig.max_tokens;
|
||||
// lets concat send messages, including 4 parts:
|
||||
// 1. long term memory: summarized memory messages
|
||||
// 2. pre-defined in-context prompts
|
||||
// 3. short term memory: latest n messages
|
||||
// 4. newest input message
|
||||
const memoryStartIndex = shouldSendLongTermMemory
|
||||
? Math.min(longTermMemoryStartIndex, shortTermMemoryStartIndex)
|
||||
: shortTermMemoryStartIndex;
|
||||
// and if user has cleared history messages, we should exclude the memory too.
|
||||
const contextStartIndex = Math.max(clearContextIndex, memoryStartIndex);
|
||||
const maxTokenThreshold = modelConfig.max_tokens;
|
||||
|
||||
// get recent messages as many as possible
|
||||
// get recent messages as much as possible
|
||||
const reversedRecentMessages = [];
|
||||
for (
|
||||
let i = n - 1, count = 0;
|
||||
i >= memoryStartIndex && count < threshold;
|
||||
let i = totalMessageCount - 1, tokenCount = 0;
|
||||
i >= contextStartIndex && tokenCount < maxTokenThreshold;
|
||||
i -= 1
|
||||
) {
|
||||
const msg = messages[i];
|
||||
if (!msg || msg.isError) continue;
|
||||
count += estimateTokenLength(msg.content);
|
||||
tokenCount += estimateTokenLength(msg.content);
|
||||
reversedRecentMessages.push(msg);
|
||||
}
|
||||
|
||||
// concat
|
||||
const recentMessages = context.concat(reversedRecentMessages.reverse());
|
||||
// concat all messages
|
||||
const recentMessages = [
|
||||
...longTermMemoryPrompts,
|
||||
...contextPrompts,
|
||||
...reversedRecentMessages.reverse(),
|
||||
];
|
||||
|
||||
return recentMessages;
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { StoreKey } from "../constant";
|
||||
import { DEFAULT_INPUT_TEMPLATE, StoreKey } from "../constant";
|
||||
|
||||
export enum SubmitKey {
|
||||
Enter = "Enter",
|
||||
@@ -39,6 +39,7 @@ export const DEFAULT_CONFIG = {
|
||||
sendMemory: true,
|
||||
historyMessageCount: 4,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
template: DEFAULT_INPUT_TEMPLATE,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -176,14 +177,16 @@ export const useAppConfig = create<ChatConfigStore>()(
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 2,
|
||||
version: 3.1,
|
||||
migrate(persistedState, version) {
|
||||
if (version === 2) return persistedState as any;
|
||||
if (version === 3.1) return persistedState as any;
|
||||
|
||||
const state = persistedState as ChatConfig;
|
||||
state.modelConfig.sendMemory = true;
|
||||
state.modelConfig.historyMessageCount = 4;
|
||||
state.modelConfig.compressMessageLengthThreshold = 1000;
|
||||
state.modelConfig.frequency_penalty = 0;
|
||||
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
|
||||
state.dontShowMaskSplashScreen = false;
|
||||
|
||||
return state;
|
||||
|
@@ -3,7 +3,7 @@ import { persist } from "zustand/middleware";
|
||||
import { BUILTIN_MASKS } from "../masks";
|
||||
import { getLang, Lang } from "../locales";
|
||||
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { ModelConfig, useAppConfig } from "./config";
|
||||
import { StoreKey } from "../constant";
|
||||
|
||||
export type Mask = {
|
||||
@@ -89,7 +89,18 @@ export const useMaskStore = create<MaskStore>()(
|
||||
const userMasks = Object.values(get().masks).sort(
|
||||
(a, b) => b.id - a.id,
|
||||
);
|
||||
return userMasks.concat(BUILTIN_MASKS);
|
||||
const config = useAppConfig.getState();
|
||||
const buildinMasks = BUILTIN_MASKS.map(
|
||||
(m) =>
|
||||
({
|
||||
...m,
|
||||
modelConfig: {
|
||||
...config.modelConfig,
|
||||
...m.modelConfig,
|
||||
},
|
||||
} as Mask),
|
||||
);
|
||||
return userMasks.concat(buildinMasks);
|
||||
},
|
||||
search(text) {
|
||||
return Object.values(get().masks);
|
||||
|
@@ -844,6 +844,7 @@
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
border-radius: 6px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.markdown-body pre code,
|
||||
|
@@ -152,6 +152,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
||||
const width = getDomContentWidth(dom);
|
||||
measureDom.style.width = width + "px";
|
||||
measureDom.innerText = dom.value !== "" ? dom.value : "1";
|
||||
measureDom.style.fontSize = dom.style.fontSize;
|
||||
const endWithEmptyLine = dom.value.endsWith("\n");
|
||||
const height = parseFloat(window.getComputedStyle(measureDom).height);
|
||||
const singleLineHeight = parseFloat(
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||

|
||||
1. 在项目配置页,点开 Environmane Variables 开始配置环境变量;
|
||||
2. 依次新增名为 OPENAI_API_KEY 和 CODE 的环境变量;
|
||||
2. 依次新增名为 OPENAI_API_KEY 和 CODE ([访问密码](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)) 的环境变量;
|
||||
3. 填入环境变量对应的值;
|
||||
4. 点击 Add 确认增加环境变量;
|
||||
5. 请确保你添加了 OPENAI_API_KEY,否则无法使用;
|
||||
|
90
src-tauri/Cargo.lock
generated
90
src-tauri/Cargo.lock
generated
@@ -63,7 +63,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
@@ -114,12 +114,27 @@ version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@@ -196,7 +211,7 @@ version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"libc",
|
||||
@@ -280,6 +295,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-window-state",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -301,7 +317,7 @@ version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
@@ -317,7 +333,7 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
@@ -370,7 +386,7 @@ version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
@@ -383,7 +399,7 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -802,7 +818,7 @@ version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cairo-rs",
|
||||
"gdk-pixbuf",
|
||||
"gdk-sys",
|
||||
@@ -818,7 +834,7 @@ version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
@@ -933,7 +949,7 @@ version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@@ -963,7 +979,7 @@ version = "0.15.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
@@ -1039,7 +1055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0"
|
||||
dependencies = [
|
||||
"atk",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cairo-rs",
|
||||
"field-offset",
|
||||
"futures-channel",
|
||||
@@ -1298,7 +1314,7 @@ version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"glib",
|
||||
"javascriptcore-rs-sys",
|
||||
]
|
||||
@@ -1528,7 +1544,7 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"jni-sys",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
@@ -1699,11 +1715,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.52"
|
||||
version = "0.10.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
|
||||
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -1731,9 +1747,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.87"
|
||||
version = "0.9.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
|
||||
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1753,7 +1769,7 @@ version = "0.15.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -1943,7 +1959,7 @@ version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
@@ -2125,7 +2141,7 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2134,7 +2150,7 @@ version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2219,7 +2235,7 @@ version = "0.37.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
@@ -2281,7 +2297,7 @@ version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -2304,7 +2320,7 @@ version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cssparser",
|
||||
"derive_more",
|
||||
"fxhash",
|
||||
@@ -2503,7 +2519,7 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
@@ -2517,7 +2533,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
@@ -2626,7 +2642,7 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd3cde9c0cd2b872616bba26b818e0d6469330196869d7e5000dba96ce9431df"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cairo-rs",
|
||||
"cc",
|
||||
"cocoa",
|
||||
@@ -2806,6 +2822,20 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7b9d7a1d8896c213998949a423d5835e8e2734c6"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags 2.3.2",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.13.0"
|
||||
@@ -3313,7 +3343,7 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cairo-rs",
|
||||
"gdk",
|
||||
"gdk-sys",
|
||||
@@ -3338,7 +3368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk-sys",
|
||||
|
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "chatgpt-next-web",
|
||||
"version": "2.8.2"
|
||||
"version": "2.8.3"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
Reference in New Issue
Block a user