-
{props.title}
-
-
- {Locale.ChatItem.ChatItemCount(props.count)}
+
+ {(provided) => (
+
+
{props.title}
+
+
+ {Locale.ChatItem.ChatItemCount(props.count)}
+
+
{props.time}
+
+
+
+
- {props.time}
-
-
-
-
-
+ )}
+
);
}
export function ChatList() {
- const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
- (state) => [
+ const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
+ useChatStore((state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
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 (
-
- {sessions.map((item, i) => (
- selectSession(i)}
- onDelete={() =>
- (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
- removeSession(i)
- }
- />
- ))}
-
+
+
+ {(provided) => (
+
+ {sessions.map((item, i) => (
+ selectSession(i)}
+ onDelete={() => chatStore.deleteSession(i)}
+ />
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
);
}
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
index 5216fb255..7cd2889f7 100644
--- a/app/components/chat.module.scss
+++ b/app/components/chat.module.scss
@@ -1,5 +1,29 @@
@import "../styles/animation.scss";
+.chat-input-actions {
+ display: flex;
+ flex-wrap: wrap;
+
+ .chat-input-action {
+ display: inline-flex;
+ border-radius: 20px;
+ font-size: 12px;
+ background-color: var(--white);
+ color: var(--black);
+ border: var(--border-in-light);
+ padding: 4px 10px;
+ animation: slide-in ease 0.3s;
+ box-shadow: var(--card-shadow);
+ transition: all ease 0.3s;
+ margin-bottom: 10px;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+}
+
.prompt-toast {
position: absolute;
bottom: -50px;
@@ -63,6 +87,14 @@
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .memory-prompt-action {
+ display: flex;
+ align-items: center;
+ }
}
.memory-prompt-content {
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index aff3b08ab..381242952 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -1,24 +1,44 @@
-import { useDebouncedCallback } from "use-debounce";
-import { useState, useRef, useEffect, useLayoutEffect } from "react";
+import { useDebounce, useDebouncedCallback } from "use-debounce";
+import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
-import ExportIcon from "../icons/export.svg";
-import MenuIcon from "../icons/menu.svg";
+import RenameIcon from "../icons/rename.svg";
+import ExportIcon from "../icons/share.svg";
+import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg";
import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
+import MaxIcon from "../icons/max.svg";
+import MinIcon from "../icons/min.svg";
-import { Message, SubmitKey, useChatStore, BOT_HELLO, ROLES } from "../store";
+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 {
+ Message,
+ SubmitKey,
+ useChatStore,
+ BOT_HELLO,
+ ROLES,
+ createMessage,
+ useAccessStore,
+ Theme,
+} from "../store";
import {
copyToClipboard,
downloadAs,
+ getEmojiUrl,
isMobileScreen,
selectOrCopy,
+ autoGrowTextArea,
} from "../utils";
import dynamic from "next/dynamic";
@@ -31,11 +51,14 @@ import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
-import { Modal, showModal, showToast } from "./ui-lib";
+import { Input, Modal, showModal } from "./ui-lib";
-const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
- loading: () =>
,
-});
+const Markdown = dynamic(
+ async () => memo((await import("./markdown")).Markdown),
+ {
+ loading: () =>
,
+ },
+);
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
loading: () =>
,
@@ -45,12 +68,16 @@ export function Avatar(props: { role: Message["role"] }) {
const config = useChatStore((state) => state.config);
if (props.role !== "user") {
- return
;
+ return (
+
+
+
+ );
}
return (
-
+
);
}
@@ -60,7 +87,9 @@ function exportMessages(messages: Message[], topic: string) {
`# ${topic}\n\n` +
messages
.map((m) => {
- return m.role === "user" ? `## ${m.content}` : m.content.trim();
+ return m.role === "user"
+ ? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
+ : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
})
.join("\n\n");
const filename = `${topic}.md`;
@@ -138,6 +167,16 @@ function PromptToast(props: {
title={Locale.Context.Edit}
onClose={() => props.setShowModal(false)}
actions={[
+
}
+ bordered
+ text={Locale.Memory.Reset}
+ onClick={() =>
+ confirm(Locale.Memory.ResetConfirm) &&
+ chatStore.resetSession()
+ }
+ />,
}
@@ -148,7 +187,6 @@ function PromptToast(props: {
]}
>
<>
- {" "}
{context.map((c, i) => (
@@ -168,17 +206,18 @@ function PromptToast(props: {
))}
-
+ rows={1}
+ onInput={(e) =>
updateContextPrompt(i, {
...c,
- content: e.target.value as any,
+ content: e.currentTarget.value as any,
})
}
- >
+ />
}
className={chatStyle["context-delete-button"]}
@@ -206,8 +245,24 @@ function PromptToast(props: {
- {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
- {session.messages.length})
+
+ {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
+ {session.messages.length})
+
+
+
+ {Locale.Memory.Send}
+
+ chatStore.updateCurrentSession(
+ (session) =>
+ (session.sendMemory = !session.sendMemory),
+ )
+ }
+ >
+
{session.memoryPrompt || Locale.Memory.EmptyContent}
@@ -273,22 +328,90 @@ function useScrollToBottom() {
// for auto-scroll
const scrollRef = useRef
(null);
const [autoScroll, setAutoScroll] = useState(true);
+ const scrollToBottom = () => {
+ const dom = scrollRef.current;
+ if (dom) {
+ setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
+ }
+ };
// auto scroll
useLayoutEffect(() => {
- const dom = scrollRef.current;
- if (dom && autoScroll) {
- setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
- }
+ autoScroll && scrollToBottom();
});
return {
scrollRef,
autoScroll,
setAutoScroll,
+ scrollToBottom,
};
}
+export function ChatActions(props: {
+ showPromptModal: () => void;
+ scrollToBottom: () => void;
+ hitBottom: boolean;
+}) {
+ const chatStore = useChatStore();
+
+ // switch themes
+ const theme = chatStore.config.theme;
+ function nextTheme() {
+ const themes = [Theme.Auto, Theme.Light, Theme.Dark];
+ const themeIndex = themes.indexOf(theme);
+ const nextIndex = (themeIndex + 1) % themes.length;
+ const nextTheme = themes[nextIndex];
+ chatStore.updateConfig((config) => (config.theme = nextTheme));
+ }
+
+ // stop all responses
+ const couldStop = ControllerPool.hasPending();
+ const stopAll = () => ControllerPool.stopAll();
+
+ return (
+
+ {couldStop && (
+
+
+
+ )}
+ {!props.hitBottom && (
+
+
+
+ )}
+ {props.hitBottom && (
+
+
+
+ )}
+
+
+ {theme === Theme.Auto ? (
+
+ ) : theme === Theme.Light ? (
+
+ ) : theme === Theme.Dark ? (
+
+ ) : null}
+
+
+ );
+}
+
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
@@ -304,9 +427,10 @@ export function Chat(props: {
const inputRef = useRef(null);
const [userInput, setUserInput] = useState("");
+ const [beforeInput, setBeforeInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
- const { scrollRef, setAutoScroll } = useScrollToBottom();
+ const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(false);
const onChatBodyScroll = (e: HTMLElement) => {
@@ -331,20 +455,30 @@ export function Chat(props: {
inputRef.current?.focus();
};
- const scrollInput = () => {
- const dom = inputRef.current;
- if (!dom) return;
- const paddingBottomNum: number = parseInt(
- window.getComputedStyle(dom).paddingBottom,
- 10,
- );
- dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
- };
+ // auto grow input
+ const [inputRows, setInputRows] = useState(2);
+ const measure = useDebouncedCallback(
+ () => {
+ 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
const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => {
- scrollInput();
setUserInput(text);
const n = text.trim().length;
@@ -355,9 +489,6 @@ export function Chat(props: {
// check if need to trigger auto completion
if (text.startsWith("/")) {
let searchText = text.slice(1);
- if (searchText.length === 0) {
- searchText = " ";
- }
onSearch(searchText);
}
}
@@ -368,6 +499,7 @@ export function Chat(props: {
if (userInput.length <= 0) return;
setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
+ setBeforeInput(userInput);
setUserInput("");
setPromptHints([]);
if (!isMobileScreen()) inputRef.current?.focus();
@@ -375,12 +507,18 @@ export function Chat(props: {
};
// stop response
- const onUserStop = (messageIndex: number) => {
- ControllerPool.stop(sessionIndex, messageIndex);
+ const onUserStop = (messageId: number) => {
+ ControllerPool.stop(sessionIndex, messageId);
};
// check if should send message
const onInputKeyDown = (e: React.KeyboardEvent) => {
+ // if ArrowUp and no userInput
+ if (e.key === "ArrowUp" && userInput.length <= 0) {
+ setUserInput(beforeInput);
+ e.preventDefault();
+ return;
+ }
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
@@ -398,29 +536,61 @@ export function Chat(props: {
}
};
- const onResend = (botIndex: number) => {
+ const findLastUesrIndex = (messageId: number) => {
// find last user input message and resend
- for (let i = botIndex; i >= 0; i -= 1) {
- if (messages[i].role === "user") {
- setIsLoading(true);
- chatStore
- .onUserInput(messages[i].content)
- .then(() => setIsLoading(false));
- inputRef.current?.focus();
- return;
+ let lastUserMessageIndex: number | null = null;
+ for (let i = 0; i < session.messages.length; i += 1) {
+ const message = session.messages[i];
+ if (message.id === messageId) {
+ break;
+ }
+ if (message.role === "user") {
+ lastUserMessageIndex = i;
}
}
+
+ return lastUserMessageIndex;
+ };
+
+ const deleteMessage = (userIndex: number) => {
+ chatStore.updateCurrentSession((session) =>
+ session.messages.splice(userIndex, 2),
+ );
+ };
+
+ const onDelete = (botMessageId: number) => {
+ const userIndex = findLastUesrIndex(botMessageId);
+ if (userIndex === null) return;
+ deleteMessage(userIndex);
+ };
+
+ const onResend = (botMessageId: number) => {
+ // find last user input message and resend
+ const userIndex = findLastUesrIndex(botMessageId);
+ if (userIndex === null) return;
+
+ setIsLoading(true);
+ const content = session.messages[userIndex].content;
+ deleteMessage(userIndex);
+ chatStore.onUserInput(content).then(() => setIsLoading(false));
+ inputRef.current?.focus();
};
const config = useChatStore((state) => state.config);
const context: RenderMessage[] = session.context.slice();
+ const accessStore = useAccessStore();
+
if (
context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content
) {
- context.push(BOT_HELLO);
+ const copiedHello = Object.assign({}, BOT_HELLO);
+ if (!accessStore.isAuthorized()) {
+ copiedHello.content = Locale.Error.Unauthorized;
+ }
+ context.push(copiedHello);
}
// preview messages
@@ -430,9 +600,10 @@ export function Chat(props: {
isLoading
? [
{
- role: "assistant",
- content: "……",
- date: new Date().toLocaleString(),
+ ...createMessage({
+ role: "assistant",
+ content: "……",
+ }),
preview: true,
},
]
@@ -442,9 +613,10 @@ export function Chat(props: {
userInput.length > 0 && config.sendPreviewBubble
? [
{
- role: "user",
- content: userInput,
- date: new Date().toLocaleString(),
+ ...createMessage({
+ role: "user",
+ content: userInput,
+ }),
preview: true,
},
]
@@ -453,29 +625,27 @@ export function Chat(props: {
const [showPromptModal, setShowPromptModal] = useState(false);
+ const renameSession = () => {
+ const newTopic = prompt(Locale.Chat.Rename, session.topic);
+ if (newTopic && newTopic !== session.topic) {
+ chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
+ }
+ };
+
// Auto focus
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
inputRef.current?.focus();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
-
+
{
- const newTopic = prompt(Locale.Chat.Rename, session.topic);
- if (newTopic && newTopic !== session.topic) {
- chatStore.updateCurrentSession(
- (session) => (session.topic = newTopic!),
- );
- }
- }}
+ onClickCapture={renameSession}
>
{session.topic}
@@ -486,7 +656,7 @@ export function Chat(props: {
}
+ icon={ }
bordered
title={Locale.Chat.Actions.ChatList}
onClick={props?.showSideBar}
@@ -494,12 +664,9 @@ export function Chat(props: {
}
+ icon={ }
bordered
- title={Locale.Chat.Actions.CompressedHistory}
- onClick={() => {
- setShowPromptModal(true);
- }}
+ onClick={renameSession}
/>
@@ -515,6 +682,19 @@ export function Chat(props: {
}}
/>
+ {!isMobileScreen() && (
+
+ : }
+ bordered
+ onClick={() => {
+ chatStore.updateConfig(
+ (config) => (config.tightBorder = !config.tightBorder),
+ );
+ }}
+ />
+
+ )}
onChatBodyScroll(e.currentTarget)}
- onWheel={() => setAutoScroll(false)}
+ onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
onTouchStart={() => {
inputRef.current?.blur();
setAutoScroll(false);
@@ -560,17 +740,25 @@ export function Chat(props: {
{message.streaming ? (
onUserStop(i)}
+ onClick={() => onUserStop(message.id ?? i)}
>
{Locale.Chat.Actions.Stop}
) : (
- onResend(i)}
- >
- {Locale.Chat.Actions.Retry}
-
+ <>
+ onDelete(message.id ?? i)}
+ >
+ {Locale.Chat.Actions.Delete}
+
+ onResend(message.id ?? i)}
+ >
+ {Locale.Chat.Actions.Retry}
+
+ >
)}
)}
- {(message.preview || message.content.length === 0) &&
- !isUser ? (
-
- ) : (
- onRightClick(e, message)}
- onDoubleClickCapture={() => {
- if (!isMobileScreen()) return;
- setUserInput(message.content);
- }}
- >
-
-
- )}
+ onRightClick(e, message)}
+ onDoubleClickCapture={() => {
+ if (!isMobileScreen()) return;
+ setUserInput(message.content);
+ }}
+ fontSize={fontSize}
+ parentRef={scrollRef}
+ />
{!isUser && !message.preview && (
@@ -618,12 +804,17 @@ export function Chat(props: {
+
+
setShowPromptModal(true)}
+ scrollToBottom={scrollToBottom}
+ hitBottom={hitBottom}
+ />
+
+ onDragMouseDown(e as any)}
+ >
diff --git a/app/components/input-range.module.scss b/app/components/input-range.module.scss
new file mode 100644
index 000000000..5a555a457
--- /dev/null
+++ b/app/components/input-range.module.scss
@@ -0,0 +1,7 @@
+.input-range {
+ border: var(--border-in-light);
+ border-radius: 10px;
+ padding: 5px 15px 5px 10px;
+ font-size: 12px;
+ display: flex;
+}
diff --git a/app/components/input-range.tsx b/app/components/input-range.tsx
new file mode 100644
index 000000000..a8ee9532b
--- /dev/null
+++ b/app/components/input-range.tsx
@@ -0,0 +1,37 @@
+import * as React from "react";
+import styles from "./input-range.module.scss";
+
+interface InputRangeProps {
+ onChange: React.ChangeEventHandler
;
+ 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 (
+
+ {title || value}
+
+
+ );
+}
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index 88e0f66f7..25d0584f6 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -8,6 +8,8 @@ import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils";
+import LoadingIcon from "../icons/three-dots.svg";
+
export function PreCode(props: { children: any }) {
const ref = useRef(null);
@@ -27,48 +29,78 @@ export function PreCode(props: { children: any }) {
);
}
-const useLazyLoad = (ref: RefObject): boolean => {
- const [isIntersecting, setIntersecting] = useState(false);
+export function Markdown(
+ props: {
+ content: string;
+ loading?: boolean;
+ fontSize?: number;
+ parentRef: RefObject;
+ } & React.DOMAttributes,
+) {
+ const mdRef = useRef(null);
+
+ const parent = props.parentRef.current;
+ const md = mdRef.current;
+ const rendered = useRef(true); // disable lazy loading for bad ux
+ const [counter, setCounter] = useState(0);
useEffect(() => {
- const observer = new IntersectionObserver(([entry]) => {
- if (entry.isIntersecting) {
- setIntersecting(true);
- observer.disconnect();
+ // to triggr rerender
+ setCounter(counter + 1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.loading]);
+
+ const inView =
+ rendered.current ||
+ (() => {
+ if (parent && md) {
+ const parentBounds = parent.getBoundingClientRect();
+ const mdBounds = md.getBoundingClientRect();
+ const isInRange = (x: number) =>
+ x <= parentBounds.bottom && x >= parentBounds.top;
+ const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
+
+ if (inView) {
+ rendered.current = true;
+ }
+
+ return inView;
}
- });
+ })();
- if (ref.current) {
- observer.observe(ref.current);
- }
+ const shouldLoading = props.loading || !inView;
- return () => {
- observer.disconnect();
- };
- }, [ref]);
-
- return isIntersecting;
-};
-
-export function Markdown(props: { content: string }) {
return (
-
- {props.content}
-
+ {shouldLoading ? (
+
+ ) : (
+
+ {props.content}
+
+ )}
+
);
}
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss
index ad994f68d..b7f095580 100644
--- a/app/components/settings.module.scss
+++ b/app/components/settings.module.scss
@@ -18,3 +18,77 @@
.avatar {
cursor: pointer;
}
+
+.password-input-container {
+ max-width: 50%;
+ display: flex;
+ justify-content: flex-end;
+
+ .password-eye {
+ margin-right: 4px;
+ }
+
+ .password-input {
+ min-width: 80%;
+ }
+}
+
+.user-prompt-modal {
+ min-height: 40vh;
+
+ .user-prompt-search {
+ width: 100%;
+ max-width: 100%;
+ margin-bottom: 10px;
+ background-color: var(--gray);
+ }
+
+ .user-prompt-list {
+ padding: 10px 0;
+
+ .user-prompt-item {
+ margin-bottom: 10px;
+ widows: 100%;
+
+ .user-prompt-header {
+ display: flex;
+ widows: 100%;
+ margin-bottom: 5px;
+
+ .user-prompt-title {
+ flex-grow: 1;
+ max-width: 100%;
+ margin-right: 5px;
+ padding: 5px;
+ font-size: 12px;
+ text-align: left;
+ }
+
+ .user-prompt-buttons {
+ display: flex;
+ align-items: center;
+
+ .user-prompt-button {
+ height: 100%;
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+
+ .user-prompt-content {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 5px;
+ margin-right: 10px;
+ font-size: 12px;
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .user-prompt-actions {
+ }
+}
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index 4c5adb86b..d81b5b358 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef, useMemo } from "react";
+import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
@@ -6,10 +6,13 @@ import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import CloseIcon from "../icons/close.svg";
+import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
+import EyeIcon from "../icons/eye.svg";
+import EyeOffIcon from "../icons/eye-off.svg";
-import { List, ListItem, Popover, showToast } from "./ui-lib";
+import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
import { IconButton } from "./button";
import {
@@ -19,15 +22,118 @@ import {
ALL_MODELS,
useUpdateStore,
useAccessStore,
+ ModalConfigValidator,
} from "../store";
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
-import { getCurrentVersion } from "../utils";
+import { copyToClipboard, getEmojiUrl } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
-import { SearchService, usePromptStore } from "../store/prompt";
-import { requestUsage } from "../requests";
+import { Prompt, SearchService, usePromptStore } from "../store/prompt";
+import { ErrorBoundary } from "./error";
+import { InputRange } from "./input-range";
+
+function UserPromptModal(props: { onClose?: () => void }) {
+ const promptStore = usePromptStore();
+ const userPrompts = promptStore.getUserPrompts();
+ const builtinPrompts = SearchService.builtinPrompts;
+ const allPrompts = userPrompts.concat(builtinPrompts);
+ const [searchInput, setSearchInput] = useState("");
+ const [searchPrompts, setSearchPrompts] = useState
([]);
+ const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
+
+ useEffect(() => {
+ if (searchInput.length > 0) {
+ const searchResult = SearchService.search(searchInput);
+ setSearchPrompts(searchResult);
+ } else {
+ setSearchPrompts([]);
+ }
+ }, [searchInput]);
+
+ return (
+
+
props.onClose?.()}
+ actions={[
+ promptStore.add({ title: "", content: "" })}
+ icon={ }
+ bordered
+ text={Locale.Settings.Prompt.Modal.Add}
+ />,
+ ]}
+ >
+
+
setSearchInput(e.currentTarget.value)}
+ >
+
+
+ {prompts.map((v, _) => (
+
+
+
{
+ if (v.isUser) {
+ promptStore.updateUserPrompts(
+ v.id!,
+ (prompt) => (prompt.title = e.currentTarget.value),
+ );
+ }
+ }}
+ >
+
+
+ {v.isUser && (
+ }
+ bordered
+ className={styles["user-prompt-button"]}
+ onClick={() => promptStore.remove(v.id!)}
+ />
+ )}
+ }
+ bordered
+ className={styles["user-prompt-button"]}
+ onClick={() => copyToClipboard(v.content)}
+ />
+
+
+
{
+ if (v.isUser) {
+ promptStore.updateUserPrompts(
+ v.id!,
+ (prompt) => (prompt.content = e.currentTarget.value),
+ );
+ }
+ }}
+ />
+
+ ))}
+
+
+
+
+ );
+}
function SettingItem(props: {
title: string;
@@ -47,6 +153,29 @@ function SettingItem(props: {
);
}
+function PasswordInput(props: HTMLProps) {
+ const [visible, setVisible] = useState(false);
+
+ function changeVisibility() {
+ setVisible(!visible);
+ }
+
+ return (
+
+ : }
+ onClick={changeVisibility}
+ className={styles["password-eye"]}
+ />
+
+
+ );
+}
+
export function Settings(props: { closeSettings: () => void }) {
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
@@ -60,51 +189,64 @@ export function Settings(props: { closeSettings: () => void }) {
const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false);
- const currentId = getCurrentVersion();
- const remoteId = updateStore.remoteId;
- const hasNewVersion = currentId !== remoteId;
+ const currentVersion = updateStore.version;
+ const remoteId = updateStore.remoteVersion;
+ const hasNewVersion = currentVersion !== remoteId;
function checkUpdate(force = false) {
setCheckingUpdate(true);
- updateStore.getLatestCommitId(force).then(() => {
+ updateStore.getLatestVersion(force).then(() => {
setCheckingUpdate(false);
});
}
- const [usage, setUsage] = useState<{
- used?: number;
- }>();
+ const usage = {
+ used: updateStore.used,
+ subscription: updateStore.subscription,
+ };
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
setLoadingUsage(true);
- requestUsage()
- .then((res) =>
- setUsage({
- used: res,
- }),
- )
- .finally(() => {
- setLoadingUsage(false);
- });
+ updateStore.updateUsage().finally(() => {
+ setLoadingUsage(false);
+ });
}
- useEffect(() => {
- checkUpdate();
- checkUsage();
- }, []);
-
const accessStore = useAccessStore();
const enabledAccessControl = useMemo(
() => accessStore.enabledAccessControl(),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const promptStore = usePromptStore();
const builtinCount = SearchService.count.builtin;
- const customCount = promptStore.prompts.size ?? 0;
+ const customCount = promptStore.getUserPrompts().length ?? 0;
+ const [shouldShowPromptModal, setShowPromptModal] = useState(false);
+
+ const showUsage = accessStore.isAuthorized();
+ useEffect(() => {
+ // checks per minutes
+ checkUpdate();
+ showUsage && checkUsage();
+ // 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 (
- <>
+
@@ -118,7 +260,14 @@ export function Settings(props: { closeSettings: () => void }) {
}
- onClick={clearSessions}
+ onClick={() => {
+ const confirmed = window.confirm(
+ `${Locale.Settings.Actions.ConfirmClearAll.Confirm}`,
+ );
+ if (confirmed) {
+ clearSessions();
+ }
+ }}
bordered
title={Locale.Settings.Actions.ClearAll}
/>
@@ -126,7 +275,14 @@ export function Settings(props: { closeSettings: () => void }) {
}
- onClick={resetConfig}
+ onClick={() => {
+ const confirmed = window.confirm(
+ `${Locale.Settings.Actions.ConfirmResetAll.Confirm}`,
+ );
+ if (confirmed) {
+ resetConfig();
+ }
+ }}
bordered
title={Locale.Settings.Actions.ResetAll}
/>
@@ -150,6 +306,7 @@ export function Settings(props: { closeSettings: () => void }) {
{
updateConfig((config) => (config.avatar = e.unified));
setShowEmojiPicker(false);
@@ -168,7 +325,7 @@ export function Settings(props: { closeSettings: () => void }) {
void }) {
title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle}
>
- void }) {
(config.fontSize = Number.parseInt(e.currentTarget.value)),
)
}
- >
+ >
@@ -290,6 +446,103 @@ export function Settings(props: { closeSettings: () => void }) {
>
+
+
+ {enabledAccessControl ? (
+
+ {
+ accessStore.updateCode(e.currentTarget.value);
+ }}
+ />
+
+ ) : (
+ <>>
+ )}
+
+
+ {
+ accessStore.updateToken(e.currentTarget.value);
+ }}
+ />
+
+
+
+ {!showUsage || loadingUsage ? (
+
+ ) : (
+ }
+ text={Locale.Settings.Usage.Check}
+ onClick={checkUsage}
+ />
+ )}
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.historyMessageCount = e.target.valueAsNumber),
+ )
+ }
+ >
+
+
+
+
+ updateConfig(
+ (config) =>
+ (config.compressMessageLengthThreshold =
+ e.currentTarget.valueAsNumber),
+ )
+ }
+ >
+
+
+
void }) {
}
text={Locale.Settings.Prompt.Edit}
- onClick={() => showToast(Locale.WIP)}
+ onClick={() => setShowPromptModal(true)}
/>
-
- {enabledAccessControl ? (
-
- {
- accessStore.updateCode(e.currentTarget.value);
- }}
- >
-
- ) : (
- <>>
- )}
-
-
- {
- accessStore.updateToken(e.currentTarget.value);
- }}
- >
-
-
-
- {loadingUsage ? (
-
- ) : (
- }
- text={Locale.Settings.Usage.Check}
- onClick={checkUsage}
- />
- )}
-
-
-
-
- updateConfig(
- (config) =>
- (config.historyMessageCount = e.target.valueAsNumber),
- )
- }
- >
-
-
-
-
- updateConfig(
- (config) =>
- (config.compressMessageLengthThreshold =
- e.currentTarget.valueAsNumber),
- )
- }
- >
-
-
@@ -420,7 +582,9 @@ export function Settings(props: { closeSettings: () => void }) {
onChange={(e) => {
updateConfig(
(config) =>
- (config.modelConfig.model = e.currentTarget.value),
+ (config.modelConfig.model = ModalConfigValidator.model(
+ e.currentTarget.value,
+ )),
);
}}
>
@@ -435,9 +599,8 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
- void }) {
updateConfig(
(config) =>
(config.modelConfig.temperature =
- e.currentTarget.valueAsNumber),
+ ModalConfigValidator.temperature(
+ e.currentTarget.valueAsNumber,
+ )),
);
}}
- >
+ >
void }) {
updateConfig(
(config) =>
(config.modelConfig.max_tokens =
- e.currentTarget.valueAsNumber),
+ ModalConfigValidator.max_tokens(
+ e.currentTarget.valueAsNumber,
+ )),
)
}
>
@@ -472,9 +639,8 @@ export function Settings(props: { closeSettings: () => void }) {
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
- void }) {
updateConfig(
(config) =>
(config.modelConfig.presence_penalty =
- e.currentTarget.valueAsNumber),
+ ModalConfigValidator.presence_penalty(
+ e.currentTarget.valueAsNumber,
+ )),
);
}}
- >
+ >
+
+ {shouldShowPromptModal && (
+ setShowPromptModal(false)} />
+ )}
- >
+
);
}
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss
index c3ebcc02c..8965c06a0 100644
--- a/app/components/ui-lib.module.scss
+++ b/app/components/ui-lib.module.scss
@@ -9,6 +9,7 @@
.popover {
position: relative;
+ z-index: 2;
}
.popover-content {
@@ -52,7 +53,7 @@
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
- width: 50vw;
+ width: 60vw;
animation: slide-in ease 0.3s;
--modal-padding: 20px;
@@ -126,6 +127,7 @@
width: 100vw;
display: flex;
justify-content: center;
+ pointer-events: none;
.toast-content {
max-width: 80vw;
@@ -135,12 +137,40 @@
box-shadow: var(--card-shadow);
border: var(--border-in-light);
color: var(--black);
- padding: 10px 30px;
+ padding: 10px 20px;
border-radius: 50px;
margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ pointer-events: all;
+
+ .toast-action {
+ padding-left: 20px;
+ color: var(--primary);
+ opacity: 0.8;
+ border: 0;
+ background: none;
+ cursor: pointer;
+ font-family: inherit;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
}
}
+.input {
+ border: var(--border-in-light);
+ border-radius: 10px;
+ padding: 10px;
+ font-family: inherit;
+ background-color: var(--white);
+ color: var(--black);
+ resize: none;
+ min-width: 50px;
+}
+
@media only screen and (max-width: 600px) {
.modal-container {
width: 90vw;
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index b516b1810..a72aa868f 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -2,6 +2,7 @@ import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import { createRoot } from "react-dom/client";
+import React from "react";
export function Popover(props: {
children: JSX.Element;
@@ -109,17 +110,37 @@ export function showModal(props: ModalProps) {
root.render(
);
}
-export type ToastProps = { content: string };
+export type ToastProps = {
+ content: string;
+ action?: {
+ text: string;
+ onClick: () => void;
+ };
+};
export function Toast(props: ToastProps) {
return (
-
{props.content}
+
+ {props.content}
+ {props.action && (
+
+ {props.action.text}
+
+ )}
+
);
}
-export function showToast(content: string, delay = 3000) {
+export function showToast(
+ content: string,
+ action?: ToastProps["action"],
+ delay = 3000,
+) {
const div = document.createElement("div");
div.className = styles.show;
document.body.appendChild(div);
@@ -138,5 +159,19 @@ export function showToast(content: string, delay = 3000) {
close();
}, delay);
- root.render(
);
+ root.render(
);
+}
+
+export type InputProps = React.HTMLProps
& {
+ autoHeight?: boolean;
+ rows?: number;
+};
+
+export function Input(props: InputProps) {
+ return (
+
+ );
}
diff --git a/app/components/window.scss b/app/components/window.scss
index d89c9eb10..a92aed4eb 100644
--- a/app/components/window.scss
+++ b/app/components/window.scss
@@ -10,6 +10,7 @@
.window-header-title {
max-width: calc(100% - 100px);
+ overflow: hidden;
.window-header-main-title {
font-size: 20px;
diff --git a/app/config/build.ts b/app/config/build.ts
new file mode 100644
index 000000000..49205c9b9
--- /dev/null
+++ b/app/config/build.ts
@@ -0,0 +1,27 @@
+const COMMIT_ID: string = (() => {
+ try {
+ const childProcess = require("child_process");
+ return (
+ childProcess
+ // .execSync("git describe --tags --abbrev=0")
+ .execSync("git rev-parse --short HEAD")
+ .toString()
+ .trim()
+ );
+ } catch (e) {
+ console.error("[Build Config] No git or not from git repo.");
+ return "unknown";
+ }
+})();
+
+export const getBuildConfig = () => {
+ if (typeof process === "undefined") {
+ throw Error(
+ "[Server Config] you are importing a nodejs-only module outside of nodejs",
+ );
+ }
+
+ return {
+ commitId: COMMIT_ID,
+ };
+};
diff --git a/app/config/server.ts b/app/config/server.ts
new file mode 100644
index 000000000..798177e59
--- /dev/null
+++ b/app/config/server.ts
@@ -0,0 +1,42 @@
+import md5 from "spark-md5";
+
+declare global {
+ namespace NodeJS {
+ interface ProcessEnv {
+ OPENAI_API_KEY?: string;
+ CODE?: string;
+ PROXY_URL?: string;
+ VERCEL?: string;
+ }
+ }
+}
+
+const ACCESS_CODES = (function getAccessCodes(): Set {
+ const code = process.env.CODE;
+
+ try {
+ const codes = (code?.split(",") ?? [])
+ .filter((v) => !!v)
+ .map((v) => md5.hash(v.trim()));
+ return new Set(codes);
+ } catch (e) {
+ return new Set();
+ }
+})();
+
+export const getServerSideConfig = () => {
+ if (typeof process === "undefined") {
+ throw Error(
+ "[Server Config] you are importing a nodejs-only module outside of nodejs",
+ );
+ }
+
+ return {
+ apiKey: process.env.OPENAI_API_KEY,
+ code: process.env.CODE,
+ codes: ACCESS_CODES,
+ needCode: ACCESS_CODES.size > 0,
+ proxyUrl: process.env.PROXY_URL,
+ isVercel: !!process.env.VERCEL,
+ };
+};
diff --git a/app/constant.ts b/app/constant.ts
index 169a5eee5..6f08ad756 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -2,6 +2,7 @@ export const OWNER = "Yidadaa";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
-export const UPDATE_URL = `${REPO_URL}#%E4%BF%9D%E6%8C%81%E6%9B%B4%E6%96%B0-keep-updated`;
+export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
+export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
diff --git a/app/global.d.ts b/app/global.d.ts
new file mode 100644
index 000000000..bd1c062de
--- /dev/null
+++ b/app/global.d.ts
@@ -0,0 +1,11 @@
+declare module "*.jpg";
+declare module "*.png";
+declare module "*.woff2";
+declare module "*.woff";
+declare module "*.ttf";
+declare module "*.scss" {
+ const content: Record;
+ export default content;
+}
+
+declare module "*.svg";
diff --git a/app/icons/auto.svg b/app/icons/auto.svg
new file mode 100644
index 000000000..6745dfbd0
--- /dev/null
+++ b/app/icons/auto.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/bottom.svg b/app/icons/bottom.svg
new file mode 100644
index 000000000..e2cfba2c7
--- /dev/null
+++ b/app/icons/bottom.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/dark.svg b/app/icons/dark.svg
new file mode 100644
index 000000000..3eebc373e
--- /dev/null
+++ b/app/icons/dark.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/eye-off.svg b/app/icons/eye-off.svg
new file mode 100644
index 000000000..a0a44f7ac
--- /dev/null
+++ b/app/icons/eye-off.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/icons/eye.svg b/app/icons/eye.svg
new file mode 100644
index 000000000..b5df29d5b
--- /dev/null
+++ b/app/icons/eye.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/icons/light.svg b/app/icons/light.svg
new file mode 100644
index 000000000..22cfa1fff
--- /dev/null
+++ b/app/icons/light.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/max.svg b/app/icons/max.svg
new file mode 100644
index 000000000..7dab09ed2
--- /dev/null
+++ b/app/icons/max.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/icons/min.svg b/app/icons/min.svg
new file mode 100644
index 000000000..3be5cd3f2
--- /dev/null
+++ b/app/icons/min.svg
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/icons/pause.svg b/app/icons/pause.svg
new file mode 100644
index 000000000..382f7a939
--- /dev/null
+++ b/app/icons/pause.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/rename.svg b/app/icons/rename.svg
new file mode 100644
index 000000000..cee69eb8d
--- /dev/null
+++ b/app/icons/rename.svg
@@ -0,0 +1 @@
+
diff --git a/app/icons/return.svg b/app/icons/return.svg
new file mode 100644
index 000000000..eba5e78f9
--- /dev/null
+++ b/app/icons/return.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/icons/share.svg b/app/icons/share.svg
new file mode 100644
index 000000000..735b8196f
--- /dev/null
+++ b/app/icons/share.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 6b6999f83..38748ef37 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,45 +2,20 @@
import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/highlight.scss";
-import process from "child_process";
-import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
+import { getBuildConfig } from "./config/build";
-let COMMIT_ID: string | undefined;
-try {
- COMMIT_ID = process
- // .execSync("git describe --tags --abbrev=0")
- .execSync("git rev-parse --short HEAD")
- .toString()
- .trim();
-} catch (e) {
- console.error("No git or not from git repo.");
-}
+const buildConfig = getBuildConfig();
export const metadata = {
title: "ChatGPT Next Web",
description: "Your personal ChatGPT Chat Bot.",
appleWebApp: {
title: "ChatGPT Next Web",
- statusBarStyle: "black-translucent",
+ statusBarStyle: "default",
},
themeColor: "#fafafa",
};
-function Meta() {
- const metas = {
- version: COMMIT_ID ?? "unknown",
- access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
- };
-
- return (
- <>
- {Object.entries(metas).map(([k, v]) => (
-
- ))}
- >
- );
-}
-
export default function RootLayout({
children,
}: {
@@ -53,7 +28,12 @@ export default function RootLayout({
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
-
+
+
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 8f2193b3d..d0ab27ca2 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -3,7 +3,7 @@ import { SubmitKey } from "../store/app";
const cn = {
WIP: "该功能仍在开发中……",
Error: {
- Unauthorized: "现在是未授权状态,请在设置页填写授权码。",
+ Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
@@ -17,15 +17,16 @@ const cn = {
Copy: "复制",
Stop: "停止",
Retry: "重试",
+ Delete: "删除",
},
Rename: "重命名对话",
Typing: "正在输入…",
Input: (submitKey: string) => {
- var inputHints = `输入消息,${submitKey} 发送`;
+ var inputHints = `${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ",Shift + Enter 换行";
}
- return inputHints;
+ return inputHints + ",/ 触发补全";
},
Send: "发送",
},
@@ -33,15 +34,22 @@ const cn = {
Title: "导出聊天记录为 Markdown",
Copy: "全部复制",
Download: "下载文件",
+ MessageFromYou: "来自你的消息",
+ MessageFromChatGPT: "来自 ChatGPT 的消息",
},
Memory: {
Title: "历史记忆",
EmptyContent: "尚未记忆",
- Copy: "全部复制",
+ Send: "发送记忆",
+ Copy: "复制记忆",
+ Reset: "重置对话",
+ ResetConfirm: "重置后将清空当前对话记录以及历史记忆,确认重置?",
},
Home: {
NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?",
+ DeleteToast: "已删除会话",
+ Revert: "撤销",
},
Settings: {
Title: "设置",
@@ -50,6 +58,12 @@ const cn = {
ClearAll: "清除所有数据",
ResetAll: "重置所有选项",
Close: "关闭",
+ ConfirmResetAll: {
+ Confirm: "确认清除所有配置?",
+ },
+ ConfirmClearAll: {
+ Confirm: "确认清除所有聊天记录?",
+ },
},
Lang: {
Name: "Language",
@@ -59,6 +73,9 @@ const cn = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "头像",
@@ -77,7 +94,7 @@ const cn = {
},
SendKey: "发送键",
Theme: "主题",
- TightBorder: "紧凑边框",
+ TightBorder: "无边框模式",
SendPreviewBubble: "发送预览气泡",
Prompt: {
Disable: {
@@ -88,6 +105,11 @@ const cn = {
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom} 条`,
Edit: "编辑",
+ Modal: {
+ Title: "提示词列表",
+ Add: "增加一条",
+ Search: "搜尋提示詞",
+ },
},
HistoryCount: {
Title: "附带历史消息数",
@@ -99,26 +121,27 @@ const cn = {
},
Token: {
Title: "API Key",
- SubTitle: "使用自己的 Key 可绕过受控访问限制",
+ SubTitle: "使用自己的 Key 可绕过密码访问限制",
Placeholder: "OpenAI API Key",
},
Usage: {
- Title: "账户余额",
- SubTitle(used: any) {
- return `本月已使用 $${used}`;
+ Title: "余额查询",
+ SubTitle(used: any, total: any) {
+ return `本月已使用 $${used},订阅总额 $${total}`;
},
IsChecking: "正在检查…",
Check: "重新检查",
+ NoAccess: "输入 API Key 或访问密码查看余额",
},
AccessCode: {
- Title: "访问码",
- SubTitle: "现在是受控访问状态",
- Placeholder: "请输入访问码",
+ Title: "访问密码",
+ SubTitle: "已开启加密访问",
+ Placeholder: "请输入访问密码",
},
Model: "模型 (model)",
Temperature: {
Title: "随机性 (temperature)",
- SubTitle: "值越大,回复越随机",
+ SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码",
},
MaxTokens: {
Title: "单次回复限制 (max_tokens)",
diff --git a/app/locales/de.ts b/app/locales/de.ts
new file mode 100644
index 000000000..e71abfaf7
--- /dev/null
+++ b/app/locales/de.ts
@@ -0,0 +1,189 @@
+import { SubmitKey } from "../store/app";
+import type { LocaleType } from "./index";
+
+const de: LocaleType = {
+ WIP: "In Bearbeitung...",
+ Error: {
+ Unauthorized:
+ "Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.",
+ },
+ ChatItem: {
+ ChatItemCount: (count: number) => `${count} Nachrichten`,
+ },
+ Chat: {
+ SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
+ Actions: {
+ ChatList: "Zur Chat-Liste gehen",
+ CompressedHistory: "Komprimierter Gedächtnis-Prompt",
+ Export: "Alle Nachrichten als Markdown exportieren",
+ Copy: "Kopieren",
+ Stop: "Stop",
+ Retry: "Wiederholen",
+ Delete: "Delete",
+ },
+ Rename: "Chat umbenennen",
+ Typing: "Tippen...",
+ Input: (submitKey: string) => {
+ var inputHints = `${submitKey} um zu Senden`;
+ if (submitKey === String(SubmitKey.Enter)) {
+ inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
+ }
+ return inputHints + ", / zum Durchsuchen von Prompts";
+ },
+ Send: "Senden",
+ },
+ Export: {
+ Title: "Alle Nachrichten",
+ Copy: "Alles kopieren",
+ Download: "Herunterladen",
+ MessageFromYou: "Deine Nachricht",
+ MessageFromChatGPT: "Nachricht von ChatGPT",
+ },
+ Memory: {
+ Title: "Gedächtnis-Prompt",
+ EmptyContent: "Noch nichts.",
+ Send: "Gedächtnis senden",
+ Copy: "Gedächtnis kopieren",
+ Reset: "Sitzung zurücksetzen",
+ ResetConfirm:
+ "Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
+ },
+ Home: {
+ NewChat: "Neuer Chat",
+ DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
+ DeleteToast: "Chat gelöscht",
+ Revert: "Zurücksetzen",
+ },
+ Settings: {
+ Title: "Einstellungen",
+ SubTitle: "Alle Einstellungen",
+ Actions: {
+ ClearAll: "Alle Daten löschen",
+ ResetAll: "Alle Einstellungen zurücksetzen",
+ Close: "Schließen",
+ ConfirmResetAll: {
+ Confirm: "Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Möchten Sie wirklich alle Chats zurücksetzen?",
+ },
+ },
+ Lang: {
+ Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
+ Options: {
+ cn: "简体中文",
+ en: "English",
+ tw: "繁體中文",
+ es: "Español",
+ it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
+ },
+ },
+ Avatar: "Avatar",
+ FontSize: {
+ Title: "Schriftgröße",
+ SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
+ },
+ Update: {
+ Version: (x: string) => `Version: ${x}`,
+ IsLatest: "Neueste Version",
+ CheckUpdate: "Update prüfen",
+ IsChecking: "Update wird geprüft...",
+ FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
+ GoToUpdate: "Aktualisieren",
+ },
+ SendKey: "Senden-Taste",
+ Theme: "Erscheinungsbild",
+ TightBorder: "Enger Rahmen",
+ SendPreviewBubble: "Vorschau-Bubble senden",
+ Prompt: {
+ Disable: {
+ Title: "Autovervollständigung deaktivieren",
+ SubTitle: "Autovervollständigung mit / starten",
+ },
+ List: "Prompt-Liste",
+ ListCount: (builtin: number, custom: number) =>
+ `${builtin} integriert, ${custom} benutzerdefiniert`,
+ Edit: "Bearbeiten",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
+ },
+ HistoryCount: {
+ Title: "Anzahl der angehängten Nachrichten",
+ SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
+ },
+ CompressThreshold: {
+ Title: "Schwellenwert für Verlaufskomprimierung",
+ SubTitle:
+ "Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
+ },
+ Token: {
+ Title: "API-Schlüssel",
+ SubTitle:
+ "Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
+ Placeholder: "OpenAI API-Schlüssel",
+ },
+ Usage: {
+ Title: "Kontostand",
+ SubTitle(used: any, total: any) {
+ return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
+ },
+ IsChecking: "Wird überprüft...",
+ Check: "Erneut prüfen",
+ NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
+ },
+ AccessCode: {
+ Title: "Zugangscode",
+ SubTitle: "Zugangskontrolle aktiviert",
+ Placeholder: "Zugangscode erforderlich",
+ },
+ Model: "Modell",
+ Temperature: {
+ Title: "Temperature", //Temperatur
+ SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
+ },
+ MaxTokens: {
+ Title: "Max Tokens", //Maximale Token
+ SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
+ },
+ PresencePenlty: {
+ Title: "Presence Penalty", //Anwesenheitsstrafe
+ SubTitle:
+ "Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
+ },
+ },
+ Store: {
+ DefaultTopic: "Neues Gespräch",
+ BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
+ Error:
+ "Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
+ Prompt: {
+ History: (content: string) =>
+ "Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
+ content,
+ Topic:
+ "Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
+ Summarize:
+ "Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
+ },
+ ConfirmClearAll:
+ "Bestätigen Sie, um alle Chat- und Einstellungsdaten zu löschen?",
+ },
+ Copy: {
+ Success: "In die Zwischenablage kopiert",
+ Failed:
+ "Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
+ },
+ Context: {
+ Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
+ Edit: "Kontext- und Gedächtnis-Prompts",
+ Add: "Hinzufügen",
+ },
+};
+
+export default de;
diff --git a/app/locales/en.ts b/app/locales/en.ts
index d8e9c615e..20e569606 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -19,15 +19,16 @@ const en: LocaleType = {
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
+ Delete: "Delete",
},
Rename: "Rename Chat",
Typing: "Typing…",
Input: (submitKey: string) => {
- var inputHints = `Type something and press ${submitKey} to send`;
+ var inputHints = `${submitKey} to send`;
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",
},
@@ -35,15 +36,23 @@ const en: LocaleType = {
Title: "All Messages",
Copy: "Copy All",
Download: "Download",
+ MessageFromYou: "Message From You",
+ MessageFromChatGPT: "Message From ChatGPT",
},
Memory: {
Title: "Memory Prompt",
EmptyContent: "Nothing yet.",
- Copy: "Copy All",
+ Send: "Send Memory",
+ Copy: "Copy Memory",
+ Reset: "Reset Session",
+ ResetConfirm:
+ "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
},
Home: {
NewChat: "New Chat",
DeleteChat: "Confirm to delete the selected conversation?",
+ DeleteToast: "Chat Deleted",
+ Revert: "Revert",
},
Settings: {
Title: "Settings",
@@ -52,6 +61,12 @@ const en: LocaleType = {
ClearAll: "Clear All Data",
ResetAll: "Reset All Settings",
Close: "Close",
+ ConfirmResetAll: {
+ Confirm: "Are you sure you want to reset all configurations?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Are you sure you want to reset all chat?",
+ },
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
@@ -61,6 +76,9 @@ const en: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -89,6 +107,11 @@ const en: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Attached Messages Count",
@@ -106,11 +129,12 @@ const en: LocaleType = {
},
Usage: {
Title: "Account Balance",
- SubTitle(used: any) {
- return `Used this month $${used}`;
+ SubTitle(used: any, total: any) {
+ return `Used this month $${used}, subscription $${total}`;
},
IsChecking: "Checking...",
- Check: "Check Again",
+ Check: "Check",
+ NoAccess: "Enter API Key to check balance",
},
AccessCode: {
Title: "Access Code",
diff --git a/app/locales/es.ts b/app/locales/es.ts
index 34fcec76c..e2a9eb211 100644
--- a/app/locales/es.ts
+++ b/app/locales/es.ts
@@ -19,6 +19,7 @@ const es: LocaleType = {
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
+ Delete: "Delete",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
@@ -35,15 +36,23 @@ const es: LocaleType = {
Title: "Todos los mensajes",
Copy: "Copiar todo",
Download: "Descargar",
+ MessageFromYou: "Mensaje de ti",
+ MessageFromChatGPT: "Mensaje de ChatGPT",
},
Memory: {
Title: "Historial de memoria",
EmptyContent: "Aún no hay nada.",
Copy: "Copiar todo",
+ Send: "Send Memory",
+ Reset: "Reset Session",
+ ResetConfirm:
+ "Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
},
Home: {
NewChat: "Nuevo chat",
DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
+ DeleteToast: "Chat Deleted",
+ Revert: "Revert",
},
Settings: {
Title: "Configuración",
@@ -52,6 +61,12 @@ const es: LocaleType = {
ClearAll: "Borrar todos los datos",
ResetAll: "Restablecer todas las configuraciones",
Close: "Cerrar",
+ ConfirmResetAll: {
+ Confirm: "Are you sure you want to reset all configurations?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Are you sure you want to reset all chat?",
+ },
},
Lang: {
Name: "Language",
@@ -61,6 +76,9 @@ const es: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -89,6 +107,11 @@ const es: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`,
Edit: "Editar",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Cantidad de mensajes adjuntos",
@@ -106,11 +129,12 @@ const es: LocaleType = {
},
Usage: {
Title: "Saldo de la cuenta",
- SubTitle(used: any) {
- return `Usado $${used}`;
+ SubTitle(used: any, total: any) {
+ return `Usado $${used}, subscription $${total}`;
},
IsChecking: "Comprobando...",
Check: "Comprobar de nuevo",
+ NoAccess: "Introduzca la clave API para comprobar el saldo",
},
AccessCode: {
Title: "Código de acceso",
diff --git a/app/locales/index.ts b/app/locales/index.ts
index 5c41eeb77..389304f85 100644
--- a/app/locales/index.ts
+++ b/app/locales/index.ts
@@ -3,10 +3,22 @@ import EN from "./en";
import TW from "./tw";
import ES from "./es";
import IT from "./it";
+import TR from "./tr";
+import JP from "./jp";
+import DE from "./de";
export type { LocaleType } from "./cn";
-export const AllLangs = ["en", "cn", "tw", "es", "it"] as const;
+export const AllLangs = [
+ "en",
+ "cn",
+ "tw",
+ "es",
+ "it",
+ "tr",
+ "jp",
+ "de",
+] as const;
type Lang = (typeof AllLangs)[number];
const LANG_KEY = "lang";
@@ -42,17 +54,13 @@ export function getLang(): Lang {
const lang = getLanguage();
- if (lang.includes("zh") || lang.includes("cn")) {
- return "cn";
- } else if (lang.includes("tw")) {
- return "tw";
- } else if (lang.includes("es")) {
- return "es";
- } else if (lang.includes("it")) {
- return "it";
- } else {
- return "en";
+ for (const option of AllLangs) {
+ if (lang.includes(option)) {
+ return option;
+ }
}
+
+ return "en";
}
export function changeLang(lang: Lang) {
@@ -60,4 +68,13 @@ export function changeLang(lang: Lang) {
location.reload();
}
-export default { en: EN, cn: CN, tw: TW, es: ES, it: IT }[getLang()];
+export default {
+ en: EN,
+ cn: CN,
+ tw: TW,
+ es: ES,
+ it: IT,
+ tr: TR,
+ jp: JP,
+ de: DE,
+}[getLang()] as typeof CN;
diff --git a/app/locales/it.ts b/app/locales/it.ts
index 8c4e01233..f0453b5c3 100644
--- a/app/locales/it.ts
+++ b/app/locales/it.ts
@@ -19,6 +19,7 @@ const it: LocaleType = {
Copy: "Copia",
Stop: "Stop",
Retry: "Riprova",
+ Delete: "Delete",
},
Rename: "Rinomina Chat",
Typing: "Typing…",
@@ -35,15 +36,23 @@ const it: LocaleType = {
Title: "Tutti i messaggi",
Copy: "Copia tutto",
Download: "Scarica",
+ MessageFromYou: "Messaggio da te",
+ MessageFromChatGPT: "Messaggio da ChatGPT",
},
Memory: {
Title: "Prompt di memoria",
EmptyContent: "Vuoto.",
Copy: "Copia tutto",
+ Send: "Send Memory",
+ Reset: "Reset Session",
+ ResetConfirm:
+ "Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
+ DeleteToast: "Chat Cancellata",
+ Revert: "Revert",
},
Settings: {
Title: "Impostazioni",
@@ -52,6 +61,12 @@ const it: LocaleType = {
ClearAll: "Cancella tutti i dati",
ResetAll: "Resetta tutte le impostazioni",
Close: "Chiudi",
+ ConfirmResetAll: {
+ Confirm: "Sei sicuro vuoi cancellare tutte le impostazioni?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Sei sicuro vuoi cancellare tutte le chat?",
+ },
},
Lang: {
Name: "Lingue",
@@ -61,6 +76,9 @@ const it: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -77,9 +95,9 @@ const it: LocaleType = {
GoToUpdate: "Aggiorna",
},
SendKey: "Tasto invia",
- Theme: "tema",
- TightBorder: "Bordi stretti",
- SendPreviewBubble: "Invia l'anteprima della bolla",
+ Theme: "Tema",
+ TightBorder: "Schermo intero",
+ SendPreviewBubble: "Anteprima di digitazione",
Prompt: {
Disable: {
Title: "Disabilita l'auto completamento",
@@ -89,6 +107,11 @@ const it: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Modifica",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
},
HistoryCount: {
Title: "Conteggio dei messaggi allegati",
@@ -100,18 +123,19 @@ const it: LocaleType = {
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
- Title: "Chiave API",
+ Title: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "Bilancio Account",
- SubTitle(used: any) {
- return `Usato in questo mese $${used}`;
+ SubTitle(used: any, total: any) {
+ return `Attualmente usato in questo mese $${used}, soglia massima $${total}`;
},
IsChecking: "Controllando...",
Check: "Controlla ancora",
+ NoAccess: "Inserire la chiave API per controllare il saldo",
},
AccessCode: {
Title: "Codice d'accesso",
diff --git a/app/locales/jp.ts b/app/locales/jp.ts
new file mode 100644
index 000000000..a793b5fe0
--- /dev/null
+++ b/app/locales/jp.ts
@@ -0,0 +1,187 @@
+import { SubmitKey } from "../store/app";
+
+const jp = {
+ WIP: "この機能は開発中です……",
+ Error: {
+ Unauthorized:
+ "現在は未承認状態です。左下の設定ボタンをクリックし、アクセスパスワードを入力してください。",
+ },
+ ChatItem: {
+ ChatItemCount: (count: number) => `${count} 通のチャット`,
+ },
+ Chat: {
+ SubTitle: (count: number) => `ChatGPTとの ${count} 通のチャット`,
+ Actions: {
+ ChatList: "メッセージリストを表示",
+ CompressedHistory: "圧縮された履歴プロンプトを表示",
+ Export: "チャット履歴をエクスポート",
+ Copy: "コピー",
+ Stop: "停止",
+ Retry: "リトライ",
+ Delete: "Delete",
+ },
+ Rename: "チャットの名前を変更",
+ Typing: "入力中…",
+ Input: (submitKey: string) => {
+ var inputHints = `${submitKey} で送信`;
+ if (submitKey === String(SubmitKey.Enter)) {
+ inputHints += ",Shift + Enter で改行";
+ }
+ return inputHints + ",/ で自動補完をトリガー";
+ },
+ Send: "送信",
+ },
+ Export: {
+ Title: "チャット履歴をMarkdown形式でエクスポート",
+ Copy: "すべてコピー",
+ Download: "ファイルをダウンロード",
+ MessageFromYou: "あなたからのメッセージ",
+ MessageFromChatGPT: "ChatGPTからのメッセージ",
+ },
+ Memory: {
+ Title: "履歴メモリ",
+ EmptyContent: "まだ記憶されていません",
+ Send: "メモリを送信",
+ Copy: "メモリをコピー",
+ Reset: "チャットをリセット",
+ ResetConfirm:
+ "リセット後、現在のチャット履歴と過去のメモリがクリアされます。リセットしてもよろしいですか?",
+ },
+ Home: {
+ NewChat: "新しいチャット",
+ DeleteChat: "選択したチャットを削除してもよろしいですか?",
+ DeleteToast: "チャットが削除されました",
+ Revert: "元に戻す",
+ },
+ Settings: {
+ Title: "設定",
+ SubTitle: "設定オプション",
+ Actions: {
+ ClearAll: "すべてのデータをクリア",
+ ResetAll: "すべてのオプションをリセット",
+ Close: "閉じる",
+ ConfirmResetAll: {
+ Confirm: "すべての設定をリセットしてもよろしいですか?",
+ },
+ ConfirmClearAll: {
+ Confirm: "すべてのチャットをリセットしてもよろしいですか?",
+ },
+ },
+ Lang: {
+ Name: "Language",
+ Options: {
+ cn: "简体中文",
+ en: "English",
+ tw: "繁體中文",
+ es: "Español",
+ it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
+ },
+ },
+ Avatar: "アバター",
+ FontSize: {
+ Title: "フォントサイズ",
+ SubTitle: "チャット内容のフォントサイズ",
+ },
+
+ Update: {
+ Version: (x: string) => `現在のバージョン:${x}`,
+ IsLatest: "最新バージョンです",
+ CheckUpdate: "アップデートを確認",
+ IsChecking: "アップデートを確認しています...",
+ FoundUpdate: (x: string) => `新しいバージョンが見つかりました:${x}`,
+ GoToUpdate: "更新する",
+ },
+ SendKey: "送信キー",
+ Theme: "テーマ",
+ TightBorder: "ボーダーレスモード",
+ SendPreviewBubble: "プレビューバブルの送信",
+ Prompt: {
+ Disable: {
+ Title: "プロンプトの自動補完を無効にする",
+ SubTitle:
+ "入力フィールドの先頭に / を入力すると、自動補完がトリガーされます。",
+ },
+ List: "カスタムプロンプトリスト",
+ ListCount: (builtin: number, custom: number) =>
+ `組み込み ${builtin} 件、ユーザー定義 ${custom} 件`,
+ Edit: "編集",
+ Modal: {
+ Title: "提示词列表",
+ Add: "增加一条",
+ Search: "搜尋提示詞",
+ },
+ },
+ 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: "アクセスパスワードを入力してください",
+ },
+ Model: "モデル (model)",
+ Temperature: {
+ Title: "ランダム性 (temperature)",
+ SubTitle:
+ "値が大きいほど、回答がランダムになります。1以上の値には文字化けが含まれる可能性があります。",
+ },
+ MaxTokens: {
+ Title: "シングルレスポンス制限 (max_tokens)",
+ SubTitle: "1回のインタラクションで使用される最大トークン数",
+ },
+ PresencePenlty: {
+ Title: "トピックの新鮮度 (presence_penalty)",
+ SubTitle: "値が大きいほど、新しいトピックへの展開が可能になります。",
+ },
+ },
+ Store: {
+ DefaultTopic: "新しいチャット",
+ BotHello: "何かお手伝いできることはありますか",
+ Error: "エラーが発生しました。しばらくしてからやり直してください。",
+ Prompt: {
+ History: (content: string) =>
+ "これは、AI とユーザの過去のチャットを要約した前提となるストーリーです:" +
+ content,
+ Topic:
+ "4~5文字でこの文章の簡潔な主題を返してください。説明、句読点、感嘆詞、余分なテキストは無しで。もし主題がない場合は、「おしゃべり」を返してください",
+ Summarize:
+ "あなたとユーザの会話を簡潔にまとめて、後続のコンテキストプロンプトとして使ってください。200字以内に抑えてください。",
+ },
+ ConfirmClearAll:
+ "すべてのチャット、設定データをクリアしてもよろしいですか?",
+ },
+ Copy: {
+ Success: "クリップボードに書き込みました",
+ Failed: "コピーに失敗しました。クリップボード許可を与えてください。",
+ },
+ Context: {
+ Toast: (x: any) => `前置コンテキストが ${x} 件設定されました`,
+ Edit: "前置コンテキストと履歴メモリ",
+ Add: "新規追加",
+ },
+};
+
+export default jp;
diff --git a/app/locales/tr.ts b/app/locales/tr.ts
new file mode 100644
index 000000000..04a846245
--- /dev/null
+++ b/app/locales/tr.ts
@@ -0,0 +1,188 @@
+import { SubmitKey } from "../store/app";
+import type { LocaleType } from "./index";
+
+const tr: LocaleType = {
+ WIP: "Çalışma devam ediyor...",
+ Error: {
+ Unauthorized:
+ "Yetkisiz erişim, lütfen erişim kodunu ayarlar sayfasından giriniz.",
+ },
+ ChatItem: {
+ ChatItemCount: (count: number) => `${count} mesaj`,
+ },
+ Chat: {
+ SubTitle: (count: number) => `ChatGPT tarafından ${count} mesaj`,
+ Actions: {
+ ChatList: "Sohbet Listesine Git",
+ CompressedHistory: "Sıkıştırılmış Geçmiş Bellek Komutu",
+ Export: "Tüm Mesajları Markdown Olarak Dışa Aktar",
+ Copy: "Kopyala",
+ Stop: "Durdur",
+ Retry: "Tekrar Dene",
+ Delete: "Delete",
+ },
+ Rename: "Sohbeti Yeniden Adlandır",
+ Typing: "Yazıyor…",
+ Input: (submitKey: string) => {
+ var inputHints = `Göndermek için ${submitKey}`;
+ if (submitKey === String(SubmitKey.Enter)) {
+ inputHints += ", kaydırmak için Shift + Enter";
+ }
+ return inputHints + ", komutları aramak için / (eğik çizgi)";
+ },
+ Send: "Gönder",
+ },
+ Export: {
+ Title: "Tüm Mesajlar",
+ Copy: "Tümünü Kopyala",
+ Download: "İndir",
+ MessageFromYou: "Sizin Mesajınız",
+ MessageFromChatGPT: "ChatGPT'nin Mesajı",
+ },
+ Memory: {
+ Title: "Bellek Komutları",
+ EmptyContent: "Henüz değil.",
+ Send: "Belleği Gönder",
+ Copy: "Belleği Kopyala",
+ Reset: "Oturumu Sıfırla",
+ ResetConfirm:
+ "Sıfırlama, geçerli görüşme geçmişini ve geçmiş belleği siler. Sıfırlamak istediğinizden emin misiniz?",
+ },
+ Home: {
+ NewChat: "Yeni Sohbet",
+ DeleteChat: "Seçili sohbeti silmeyi onaylıyor musunuz?",
+ DeleteToast: "Sohbet Silindi",
+ Revert: "Geri Al",
+ },
+ Settings: {
+ Title: "Ayarlar",
+ SubTitle: "Tüm Ayarlar",
+ Actions: {
+ ClearAll: "Tüm Verileri Temizle",
+ ResetAll: "Tüm Ayarları Sıfırla",
+ Close: "Kapat",
+ ConfirmResetAll: {
+ Confirm: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?",
+ },
+ },
+ Lang: {
+ Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
+ Options: {
+ cn: "简体中文",
+ en: "English",
+ tw: "繁體中文",
+ es: "Español",
+ it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
+ },
+ },
+ Avatar: "Avatar",
+ FontSize: {
+ Title: "Yazı Boyutu",
+ SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
+ },
+ Update: {
+ Version: (x: string) => `Sürüm: ${x}`,
+ IsLatest: "En son sürüm",
+ CheckUpdate: "Güncellemeyi Kontrol Et",
+ IsChecking: "Güncelleme kontrol ediliyor...",
+ FoundUpdate: (x: string) => `Yeni sürüm bulundu: ${x}`,
+ GoToUpdate: "Güncelle",
+ },
+ SendKey: "Gönder Tuşu",
+ Theme: "Tema",
+ TightBorder: "Tam Ekran",
+ SendPreviewBubble: "Mesaj Önizleme Balonu",
+ Prompt: {
+ Disable: {
+ Title: "Otomatik tamamlamayı devre dışı bırak",
+ SubTitle: "Otomatik tamamlamayı kullanmak için / (eğik çizgi) girin",
+ },
+ List: "Komut Listesi",
+ ListCount: (builtin: number, custom: number) =>
+ `${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
+ Edit: "Düzenle",
+ Modal: {
+ Title: "Prompt List",
+ Add: "Add One",
+ Search: "Search Prompts",
+ },
+ },
+ HistoryCount: {
+ Title: "Ekli Mesaj Sayısı",
+ SubTitle: "İstek başına ekli gönderilen mesaj sayısı",
+ },
+ CompressThreshold: {
+ Title: "Geçmiş Sıkıştırma Eşiği",
+ SubTitle:
+ "Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
+ },
+ Token: {
+ Title: "API Anahtarı",
+ SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın",
+ Placeholder: "OpenAI API Anahtarı",
+ },
+ Usage: {
+ Title: "Hesap Bakiyesi",
+ SubTitle(used: any, total: any) {
+ return `Bu ay kullanılan $${used}, abonelik $${total}`;
+ },
+ IsChecking: "Kontrol ediliyor...",
+ Check: "Tekrar Kontrol Et",
+ NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
+ },
+ AccessCode: {
+ Title: "Erişim Kodu",
+ SubTitle: "Erişim kontrolü etkinleştirme",
+ Placeholder: "Erişim Kodu Gerekiyor",
+ },
+ Model: "Model",
+ Temperature: {
+ Title: "Gerçeklik",
+ SubTitle:
+ "Daha büyük bir değer girildiğinde gerçeklik oranı düşer ve daha rastgele çıktılar üretir",
+ },
+ MaxTokens: {
+ Title: "Maksimum Belirteç",
+ SubTitle:
+ "Girdi belirteçlerinin ve oluşturulan belirteçlerin maksimum uzunluğu",
+ },
+ PresencePenlty: {
+ Title: "Varlık Cezası",
+ SubTitle:
+ "Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır",
+ },
+ },
+ Store: {
+ DefaultTopic: "Yeni Konuşma",
+ BotHello: "Merhaba! Size bugün nasıl yardımcı olabilirim?",
+ Error: "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyiniz.",
+ Prompt: {
+ History: (content: string) =>
+ "Bu, yapay zeka ile kullanıcı arasındaki sohbet geçmişinin bir özetidir: " +
+ content,
+ Topic:
+ "Lütfen herhangi bir giriş, noktalama işareti, tırnak işareti, nokta, sembol veya ek metin olmadan konuşmamızı özetleyen dört ila beş kelimelik bir başlık oluşturun. Çevreleyen tırnak işaretlerini kaldırın.",
+ Summarize:
+ "Gelecekteki bağlam için bir bilgi istemi olarak kullanmak üzere tartışmamızı en fazla 200 kelimeyle özetleyin.",
+ },
+ ConfirmClearAll:
+ "Tüm sohbet ve ayar verilerini temizlemeyi onaylıyor musunuz?",
+ },
+ Copy: {
+ Success: "Panoya kopyalandı",
+ Failed: "Kopyalama başarısız oldu, lütfen panoya erişim izni verin",
+ },
+ Context: {
+ Toast: (x: any) => `${x} bağlamsal bellek komutu`,
+ Edit: "Bağlamsal ve Bellek Komutları",
+ Add: "Yeni Ekle",
+ },
+};
+
+export default tr;
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index 3779a5672..2fbb2e477 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -4,7 +4,7 @@ import type { LocaleType } from "./index";
const tw: LocaleType = {
WIP: "該功能仍在開發中……",
Error: {
- Unauthorized: "目前您的狀態是未授權,請前往設定頁面填寫授權碼。",
+ Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 條對話`,
@@ -18,6 +18,7 @@ const tw: LocaleType = {
Copy: "複製",
Stop: "停止",
Retry: "重試",
+ Delete: "刪除",
},
Rename: "重命名對話",
Typing: "正在輸入…",
@@ -34,15 +35,22 @@ const tw: LocaleType = {
Title: "匯出聊天記錄為 Markdown",
Copy: "複製全部",
Download: "下載檔案",
+ MessageFromYou: "來自你的訊息",
+ MessageFromChatGPT: "來自 ChatGPT 的訊息",
},
Memory: {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",
Copy: "複製全部",
+ Send: "發送記憶",
+ Reset: "重置對話",
+ ResetConfirm: "重置後將清空當前對話記錄以及歷史記憶,確認重置?",
},
Home: {
NewChat: "新的對話",
DeleteChat: "確定要刪除選取的對話嗎?",
+ DeleteToast: "已刪除對話",
+ Revert: "撤銷",
},
Settings: {
Title: "設定",
@@ -51,6 +59,12 @@ const tw: LocaleType = {
ClearAll: "清除所有數據",
ResetAll: "重置所有設定",
Close: "關閉",
+ ConfirmResetAll: {
+ Confirm: "Are you sure you want to reset all configurations?",
+ },
+ ConfirmClearAll: {
+ Confirm: "Are you sure you want to reset all chat?",
+ },
},
Lang: {
Name: "Language",
@@ -60,6 +74,9 @@ const tw: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
+ tr: "Türkçe",
+ jp: "日本語",
+ de: "Deutsch",
},
},
Avatar: "大頭貼",
@@ -88,6 +105,11 @@ const tw: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`內置 ${builtin} 條,用戶定義 ${custom} 條`,
Edit: "編輯",
+ Modal: {
+ Title: "提示詞列表",
+ Add: "增加一條",
+ Search: "搜索提示词",
+ },
},
HistoryCount: {
Title: "附帶歷史訊息數",
@@ -99,21 +121,22 @@ const tw: LocaleType = {
},
Token: {
Title: "API Key",
- SubTitle: "使用自己的 Key 可規避受控訪問限制",
+ SubTitle: "使用自己的 Key 可規避授權訪問限制",
Placeholder: "OpenAI API Key",
},
Usage: {
Title: "帳戶餘額",
- SubTitle(used: any) {
- return `本月已使用 $${used}`;
+ SubTitle(used: any, total: any) {
+ return `本月已使用 $${used},订阅总额 $${total}`;
},
IsChecking: "正在檢查…",
Check: "重新檢查",
+ NoAccess: "輸入API Key查看餘額",
},
AccessCode: {
- Title: "訪問碼",
- SubTitle: "現在是受控訪問狀態",
- Placeholder: "請輸入訪問碼",
+ Title: "授權碼",
+ SubTitle: "現在是未授權訪問狀態",
+ Placeholder: "請輸入授權碼",
},
Model: "模型 (model)",
Temperature: {
@@ -136,9 +159,10 @@ const tw: LocaleType = {
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
- Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
+ Topic:
+ "Summarise the conversation in a short and concise eye-catching title that instantly conveys the main topic. Use as few words as possible. Use the language used in the enquiry, e.g. use English for English enquiry, use zh-hant for traditional chinese enquiry. Don't use quotation marks at the beginning and the end.",
Summarize:
- "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
+ "Summarise the conversation in at most 250 tokens for continuing the conversation in future. Use the language used in the conversation, e.g. use English for English conversation, use zh-hant for traditional chinese conversation.",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
diff --git a/app/page.tsx b/app/page.tsx
index 1d1da2227..20b503174 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -2,11 +2,15 @@ import { Analytics } from "@vercel/analytics/react";
import { Home } from "./components/home";
-export default function App() {
+import { getServerSideConfig } from "./config/server";
+
+const serverConfig = getServerSideConfig();
+
+export default async function App() {
return (
<>
-
+ {serverConfig?.isVercel && }
>
);
}
diff --git a/app/requests.ts b/app/requests.ts
index ee3103b77..3cb838e63 100644
--- a/app/requests.ts
+++ b/app/requests.ts
@@ -1,9 +1,8 @@
-import type { ChatRequest, ChatReponse } from "./api/openai/typing";
-import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
-import Locale from "./locales";
+import type { ChatRequest, ChatResponse } from "./api/openai/typing";
+import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
import { showToast } from "./components/ui-lib";
-const TIME_OUT_MS = 30000;
+const TIME_OUT_MS = 60000;
const makeRequestParam = (
messages: Message[],
@@ -21,10 +20,16 @@ const makeRequestParam = (
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
}
+ 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 {
- model: "gpt-3.5-turbo",
messages: sendMessages,
stream: options?.stream,
+ ...modelConfig,
};
};
@@ -45,11 +50,10 @@ function getHeaders() {
export function requestOpenaiClient(path: string) {
return (body: any, method = "POST") =>
- fetch("/api/openai", {
+ fetch("/api/openai?_vercel_no_cache=1", {
method,
headers: {
"Content-Type": "application/json",
- "Cache-Control": "no-cache",
path,
...getHeaders(),
},
@@ -63,7 +67,7 @@ export async function requestChat(messages: Message[]) {
const res = await requestOpenaiClient("v1/chat/completions")(req);
try {
- const response = (await res.json()) as ChatReponse;
+ const response = (await res.json()) as ChatResponse;
return response;
} catch (error) {
console.error("[Request Chat] ", error, res.body);
@@ -76,36 +80,48 @@ export async function requestUsage() {
.getDate()
.toString()
.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 startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startDate = formatDate(startOfMonth);
const endDate = formatDate(now);
- const res = await requestOpenaiClient(
- `dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
- )(null, "GET");
- try {
- const response = (await res.json()) as {
- total_usage: number;
- error?: {
- type: string;
- message: string;
- };
+ const [used, subs] = await Promise.all([
+ requestOpenaiClient(
+ `dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
+ )(null, "GET"),
+ requestOpenaiClient("dashboard/billing/subscription")(null, "GET"),
+ ]);
+
+ const response = (await used.json()) as {
+ total_usage?: number;
+ error?: {
+ type: string;
+ message: string;
};
+ };
- if (response.error && response.error.type) {
- showToast(response.error.message);
- return;
- }
+ const total = (await subs.json()) as {
+ hard_limit_usd?: number;
+ };
- if (response.total_usage) {
- response.total_usage = Math.round(response.total_usage) / 100;
- }
- return response.total_usage;
- } catch (error) {
- console.error("[Request usage] ", error, res.body);
+ if (response.error && response.error.type) {
+ showToast(response.error.message);
+ return;
}
+
+ if (response.total_usage) {
+ response.total_usage = Math.round(response.total_usage) / 100;
+ }
+
+ if (total.hard_limit_usd) {
+ total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
+ }
+
+ return {
+ used: response.total_usage,
+ subscription: total.hard_limit_usd,
+ };
}
export async function requestChatStream(
@@ -123,11 +139,6 @@ export async function requestChatStream(
filterBot: options?.filterBot,
});
- // valid and assign model config
- if (options?.modelConfig) {
- Object.assign(req, filterConfig(options.modelConfig));
- }
-
console.log("[Request] ", req);
const controller = new AbortController();
@@ -160,14 +171,18 @@ export async function requestChatStream(
options?.onController?.(controller);
while (true) {
- // handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
- const text = decoder.decode(content?.value);
+
+ if (!content || !content.value) {
+ break;
+ }
+
+ const text = decoder.decode(content.value, { stream: true });
responseText += text;
- const done = !content || content.done;
+ const done = content.done;
options?.onMessage(responseText, false);
if (done) {
@@ -177,8 +192,8 @@ export async function requestChatStream(
finish();
} else if (res.status === 401) {
- console.error("Anauthorized");
- options?.onError(new Error("Anauthorized"), res.status);
+ console.error("Unauthorized");
+ options?.onError(new Error("Unauthorized"), res.status);
} else {
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"), res.status);
@@ -209,23 +224,30 @@ export const ControllerPool = {
addController(
sessionIndex: number,
- messageIndex: number,
+ messageId: number,
controller: AbortController,
) {
- const key = this.key(sessionIndex, messageIndex);
+ const key = this.key(sessionIndex, messageId);
this.controllers[key] = controller;
return key;
},
- stop(sessionIndex: number, messageIndex: number) {
- const key = this.key(sessionIndex, messageIndex);
+ stop(sessionIndex: number, messageId: number) {
+ const key = this.key(sessionIndex, messageId);
const controller = this.controllers[key];
- console.log(controller);
controller?.abort();
},
- remove(sessionIndex: number, messageIndex: number) {
- const key = this.key(sessionIndex, messageIndex);
+ stopAll() {
+ Object.values(this.controllers).forEach((v) => v.abort());
+ },
+
+ hasPending() {
+ return Object.values(this.controllers).length > 0;
+ },
+
+ remove(sessionIndex: number, messageId: number) {
+ const key = this.key(sessionIndex, messageId);
delete this.controllers[key];
},
diff --git a/app/store/access.ts b/app/store/access.ts
index 9c61dfa01..aed131684 100644
--- a/app/store/access.ts
+++ b/app/store/access.ts
@@ -1,25 +1,33 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import { queryMeta } from "../utils";
export interface AccessControlStore {
accessCode: string;
token: string;
+ needCode: boolean;
+
updateToken: (_: string) => void;
updateCode: (_: string) => void;
enabledAccessControl: () => boolean;
+ isAuthorized: () => boolean;
+ fetch: () => void;
}
export const ACCESS_KEY = "access-control";
+let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
+
export const useAccessStore = create()(
persist(
(set, get) => ({
token: "",
accessCode: "",
+ needCode: true,
enabledAccessControl() {
- return queryMeta("access") === "enabled";
+ get().fetch();
+
+ return get().needCode;
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
@@ -27,10 +35,35 @@ export const useAccessStore = create()(
updateToken(token: string) {
set((state) => ({ token }));
},
+ isAuthorized() {
+ // has token or has code or disabled access control
+ return (
+ !!get().token || !!get().accessCode || !get().enabledAccessControl()
+ );
+ },
+ fetch() {
+ if (fetchState > 0) return;
+ fetchState = 1;
+ fetch("/api/config", {
+ method: "post",
+ body: null,
+ })
+ .then((res) => res.json())
+ .then((res: DangerConfig) => {
+ console.log("[Config] got config from server", res);
+ set(() => ({ ...res }));
+ })
+ .catch(() => {
+ console.error("[Config] failed to fetch config");
+ })
+ .finally(() => {
+ fetchState = 2;
+ });
+ },
}),
{
name: ACCESS_KEY,
version: 1,
- }
- )
+ },
+ ),
);
diff --git a/app/store/app.ts b/app/store/app.ts
index 8070ef2d3..d414cf102 100644
--- a/app/store/app.ts
+++ b/app/store/app.ts
@@ -7,17 +7,29 @@ import {
requestChatStream,
requestWithPrompt,
} from "../requests";
-import { trimTopic } from "../utils";
+import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales";
+import { showToast } from "../components/ui-lib";
export type Message = ChatCompletionResponseMessage & {
date: string;
streaming?: boolean;
isError?: boolean;
model?: string;
+ id?: number;
};
+export function createMessage(override: Partial): Message {
+ return {
+ id: Date.now(),
+ date: new Date().toLocaleString(),
+ role: "user",
+ content: "",
+ ...override,
+ };
+}
+
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
@@ -42,6 +54,7 @@ export interface ChatConfig {
theme: Theme;
tightBorder: boolean;
sendPreviewBubble: boolean;
+ sidebarWidth: number;
disablePromptHint: boolean;
@@ -86,43 +99,39 @@ export const ALL_MODELS = [
},
];
-export function isValidModel(name: string) {
- return ALL_MODELS.some((m) => m.name === name && m.available);
+export function limitNumber(
+ x: number,
+ min: number,
+ max: number,
+ defaultValue: number,
+) {
+ if (typeof x !== "number" || isNaN(x)) {
+ return defaultValue;
+ }
+
+ return Math.min(max, Math.max(min, x));
}
-export function isValidNumber(x: number, min: number, max: number) {
- return typeof x === "number" && x <= max && x >= min;
+export function limitModel(name: string) {
+ return ALL_MODELS.some((m) => m.name === name && m.available)
+ ? name
+ : ALL_MODELS[4].name;
}
-export function filterConfig(oldConfig: ModelConfig): Partial {
- const config = Object.assign({}, oldConfig);
-
- const validator: {
- [k in keyof ModelConfig]: (x: ModelConfig[keyof ModelConfig]) => boolean;
- } = {
- model(x) {
- return isValidModel(x as string);
- },
- max_tokens(x) {
- return isValidNumber(x as number, 100, 32000);
- },
- presence_penalty(x) {
- return isValidNumber(x as number, -2, 2);
- },
- temperature(x) {
- return isValidNumber(x as number, 0, 2);
- },
- };
-
- Object.keys(validator).forEach((k) => {
- const key = k as keyof ModelConfig;
- if (!validator[key](config[key])) {
- delete config[key];
- }
- });
-
- return config;
-}
+export const ModalConfigValidator = {
+ model(x: string) {
+ return limitModel(x);
+ },
+ max_tokens(x: number) {
+ return limitNumber(x, 0, 32000, 2000);
+ },
+ presence_penalty(x: number) {
+ return limitNumber(x, -2, 2, 0);
+ },
+ temperature(x: number) {
+ return limitNumber(x, 0, 2, 1);
+ },
+};
const DEFAULT_CONFIG: ChatConfig = {
historyMessageCount: 4,
@@ -134,6 +143,7 @@ const DEFAULT_CONFIG: ChatConfig = {
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
+ sidebarWidth: 300,
disablePromptHint: false,
@@ -154,6 +164,7 @@ export interface ChatStat {
export interface ChatSession {
id: number;
topic: string;
+ sendMemory: boolean;
memoryPrompt: string;
context: Message[];
messages: Message[];
@@ -163,11 +174,10 @@ export interface ChatSession {
}
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
-export const BOT_HELLO: Message = {
+export const BOT_HELLO: Message = createMessage({
role: "assistant",
content: Locale.Store.BotHello,
- date: "",
-};
+});
function createEmptySession(): ChatSession {
const createDate = new Date().toLocaleString();
@@ -175,6 +185,7 @@ function createEmptySession(): ChatSession {
return {
id: Date.now(),
topic: DEFAULT_TOPIC,
+ sendMemory: true,
memoryPrompt: "",
context: [],
messages: [],
@@ -194,8 +205,10 @@ interface ChatStore {
currentSessionIndex: number;
clearSessions: () => void;
removeSession: (index: number) => void;
+ moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
+ deleteSession: (index?: number) => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise;
@@ -207,6 +220,7 @@ interface ChatStore {
messageIndex: number,
updater: (message?: Message) => void,
) => void;
+ resetSession: () => void;
getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message;
@@ -283,6 +297,31 @@ export const useChatStore = create()(
});
},
+ 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() {
set((state) => ({
currentSessionIndex: 0,
@@ -290,6 +329,33 @@ export const useChatStore = create()(
}));
},
+ deleteSession(i?: number) {
+ const deletedSession = get().currentSession();
+ const index = i ?? 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)),
+ ),
+ }));
+ },
+ },
+ 5000,
+ );
+ }
+ },
+
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
@@ -313,19 +379,17 @@ export const useChatStore = create()(
},
async onUserInput(content) {
- const userMessage: Message = {
+ const userMessage: Message = createMessage({
role: "user",
content,
- date: new Date().toLocaleString(),
- };
+ });
- const botMessage: Message = {
- content: "",
+ const botMessage: Message = createMessage({
role: "assistant",
- date: new Date().toLocaleString(),
streaming: true,
model: get().config.modelConfig.model,
- };
+ id: userMessage.id! + 1,
+ });
// get recent messages
const recentMessages = get().getMessagesWithMemory();
@@ -348,7 +412,10 @@ export const useChatStore = create()(
botMessage.streaming = false;
botMessage.content = content;
get().onNewMessage(botMessage);
- ControllerPool.remove(sessionIndex, messageIndex);
+ ControllerPool.remove(
+ sessionIndex,
+ botMessage.id ?? messageIndex,
+ );
} else {
botMessage.content = content;
set(() => ({}));
@@ -357,20 +424,20 @@ export const useChatStore = create()(
onError(error, statusCode) {
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
- } else {
+ } else if (!error.message.includes("aborted")) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;
userMessage.isError = true;
botMessage.isError = true;
set(() => ({}));
- ControllerPool.remove(sessionIndex, messageIndex);
+ ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
- messageIndex,
+ botMessage.id ?? messageIndex,
controller,
);
},
@@ -397,7 +464,11 @@ export const useChatStore = create()(
const context = session.context.slice();
- if (session.memoryPrompt && session.memoryPrompt.length > 0) {
+ if (
+ session.sendMemory &&
+ session.memoryPrompt &&
+ session.memoryPrompt.length > 0
+ ) {
const memoryPrompt = get().getMemoryPrompt();
context.push(memoryPrompt);
}
@@ -421,6 +492,13 @@ export const useChatStore = create()(
set(() => ({ sessions }));
},
+ resetSession() {
+ get().updateCurrentSession((session) => {
+ session.messages = [];
+ session.memoryPrompt = "";
+ });
+ },
+
summarizeSession() {
const session = get().currentSession();
@@ -433,7 +511,8 @@ export const useChatStore = create()(
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
(res) => {
get().updateCurrentSession(
- (session) => (session.topic = trimTopic(res)),
+ (session) =>
+ (session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
);
},
);
@@ -512,7 +591,7 @@ export const useChatStore = create()(
}),
{
name: LOCAL_KEY,
- version: 1.1,
+ version: 1.2,
migrate(persistedState, version) {
const state = persistedState as ChatStore;
@@ -520,6 +599,10 @@ export const useChatStore = create()(
state.sessions.forEach((s) => (s.context = []));
}
+ if (version < 1.2) {
+ state.sessions.forEach((s) => (s.sendMemory = true));
+ }
+
return state;
},
},
diff --git a/app/store/prompt.ts b/app/store/prompt.ts
index b91a38d0b..8d754ff5d 100644
--- a/app/store/prompt.ts
+++ b/app/store/prompt.ts
@@ -1,63 +1,78 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
+import { getLang } from "../locales";
export interface Prompt {
id?: number;
+ isUser?: boolean;
title: string;
content: string;
}
export interface PromptStore {
+ counter: number;
latestId: number;
- prompts: Map;
+ prompts: Record;
add: (prompt: Prompt) => number;
remove: (id: number) => void;
search: (text: string) => Prompt[];
+
+ getUserPrompts: () => Prompt[];
+ updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
- engine: new Fuse([], { keys: ["title"] }),
+ builtinEngine: new Fuse([], { keys: ["title"] }),
+ userEngine: new Fuse([], { keys: ["title"] }),
count: {
builtin: 0,
},
+ allPrompts: [] as Prompt[],
+ builtinPrompts: [] as Prompt[],
- init(prompts: Prompt[]) {
+ init(builtinPrompts: Prompt[], userPrompts: Prompt[]) {
if (this.ready) {
return;
}
- this.engine.setCollection(prompts);
+ this.allPrompts = userPrompts.concat(builtinPrompts);
+ this.builtinPrompts = builtinPrompts.slice();
+ this.builtinEngine.setCollection(builtinPrompts);
+ this.userEngine.setCollection(userPrompts);
this.ready = true;
},
remove(id: number) {
- this.engine.remove((doc) => doc.id === id);
+ this.userEngine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
- this.engine.add(prompt);
+ this.userEngine.add(prompt);
},
search(text: string) {
- const results = this.engine.search(text);
- return results.map((v) => v.item);
+ const userResults = this.userEngine.search(text);
+ const builtinResults = this.builtinEngine.search(text);
+ return userResults.concat(builtinResults).map((v) => v.item);
},
};
export const usePromptStore = create()(
persist(
(set, get) => ({
+ counter: 0,
latestId: 0,
- prompts: new Map(),
+ prompts: {},
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
- prompts.set(prompt.id, prompt);
+ prompt.isUser = true;
+ prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
@@ -69,15 +84,41 @@ export const usePromptStore = create()(
remove(id) {
const prompts = get().prompts;
- prompts.delete(id);
+ delete prompts[id];
SearchService.remove(id);
set(() => ({
prompts,
+ counter: get().counter + 1,
}));
},
+ getUserPrompts() {
+ const userPrompts = Object.values(get().prompts ?? {});
+ userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0));
+ return userPrompts;
+ },
+
+ updateUserPrompts(id: number, updater) {
+ const prompt = get().prompts[id] ?? {
+ title: "",
+ content: "",
+ id,
+ };
+
+ SearchService.remove(id);
+ updater(prompt);
+ const prompts = get().prompts;
+ prompts[id] = prompt;
+ set(() => ({ prompts }));
+ SearchService.add(prompt);
+ },
+
search(text) {
+ if (text.length === 0) {
+ // return all rompts
+ return SearchService.allPrompts.concat([...get().getUserPrompts()]);
+ }
return SearchService.search(text) as Prompt[];
},
}),
@@ -92,24 +133,31 @@ export const usePromptStore = create()(
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
- const builtinPrompts = [res.en, res.cn]
- .map((promptList: PromptList) => {
+ let fetchPrompts = [res.en, res.cn];
+ if (getLang() === "cn") {
+ fetchPrompts = fetchPrompts.reverse();
+ }
+ const builtinPrompts = fetchPrompts.map(
+ (promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
+ id: Math.random(),
title,
content,
} as Prompt),
);
- })
- .concat([...(state?.prompts?.values() ?? [])]);
-
- const allPromptsForSearch = builtinPrompts.reduce(
- (pre, cur) => pre.concat(cur),
- [],
+ },
);
+
+ const userPrompts =
+ usePromptStore.getState().getUserPrompts() ?? [];
+
+ const allPromptsForSearch = builtinPrompts
+ .reduce((pre, cur) => pre.concat(cur), [])
+ .filter((v) => !!v.title && !!v.content);
SearchService.count.builtin = res.en.length + res.cn.length;
- SearchService.init(allPromptsForSearch);
+ SearchService.init(allPromptsForSearch, userPrompts);
});
},
},
diff --git a/app/store/update.ts b/app/store/update.ts
index 97fb343c3..47b190b88 100644
--- a/app/store/update.ts
+++ b/app/store/update.ts
@@ -1,29 +1,58 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
-import { getCurrentVersion } from "../utils";
+import { requestUsage } from "../requests";
export interface UpdateStore {
lastUpdate: number;
- remoteId: string;
+ remoteVersion: string;
- getLatestCommitId: (force: boolean) => Promise;
+ used?: number;
+ subscription?: number;
+ lastUpdateUsage: number;
+
+ version: string;
+ getLatestVersion: (force?: boolean) => Promise;
+ updateUsage: (force?: boolean) => Promise;
}
export const UPDATE_KEY = "chat-update";
+function queryMeta(key: string, defaultValue?: string): string {
+ let ret: string;
+ if (document) {
+ const meta = document.head.querySelector(
+ `meta[name='${key}']`,
+ ) as HTMLMetaElement;
+ ret = meta?.content ?? "";
+ } else {
+ ret = defaultValue ?? "";
+ }
+
+ return ret;
+}
+
+const ONE_MINUTE = 60 * 1000;
+
export const useUpdateStore = create()(
persist(
(set, get) => ({
lastUpdate: 0,
- remoteId: "",
+ remoteVersion: "",
- async getLatestCommitId(force = false) {
- const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
- const shouldFetch = force || overTenMins;
- if (!shouldFetch) {
- return getCurrentVersion();
- }
+ lastUpdateUsage: 0,
+
+ version: "unknown",
+
+ async getLatestVersion(force = false) {
+ set(() => ({ version: queryMeta("version") ?? "unknown" }));
+
+ const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
+ if (!force && !overTenMins) return;
+
+ set(() => ({
+ lastUpdate: Date.now(),
+ }));
try {
// const data = await (await fetch(FETCH_TAG_URL)).json();
@@ -31,14 +60,26 @@ export const useUpdateStore = create()(
const data = await (await fetch(FETCH_COMMIT_URL)).json();
const remoteId = (data[0].sha as string).substring(0, 7);
set(() => ({
- lastUpdate: Date.now(),
- remoteId,
+ remoteVersion: remoteId,
}));
console.log("[Got Upstream] ", remoteId);
- return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
- return getCurrentVersion();
+ }
+ },
+
+ async updateUsage(force = false) {
+ const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
+ if (!overOneMinute && !force) return;
+
+ set(() => ({
+ lastUpdateUsage: Date.now(),
+ }));
+
+ const usage = await requestUsage();
+
+ if (usage) {
+ set(() => usage);
}
},
}),
diff --git a/app/styles/globals.scss b/app/styles/globals.scss
index 73b9b36f3..37c662288 100644
--- a/app/styles/globals.scss
+++ b/app/styles/globals.scss
@@ -1,4 +1,6 @@
@mixin light {
+ --theme: light;
+
/* color */
--white: white;
--black: rgb(48, 48, 48);
@@ -18,6 +20,8 @@
}
@mixin dark {
+ --theme: dark;
+
/* color */
--white: rgb(30, 30, 30);
--black: rgb(187, 187, 187);
@@ -31,6 +35,10 @@
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
+
+ div:not(.no-dark) > svg {
+ filter: invert(0.5);
+ }
}
.light {
@@ -126,8 +134,13 @@ select {
text-align: center;
}
+label {
+ cursor: pointer;
+}
+
input {
text-align: center;
+ font-family: inherit;
}
input[type="checkbox"] {
@@ -155,19 +168,11 @@ input[type="checkbox"]:checked::after {
input[type="range"] {
appearance: none;
- border: var(--border-in-light);
- border-radius: 10px;
- padding: 5px 15px 5px 10px;
background-color: var(--white);
color: var(--black);
-
- &::before {
- content: attr(value);
- font-size: 12px;
- }
}
-input[type="range"]::-webkit-slider-thumb {
+@mixin thumb() {
appearance: none;
height: 8px;
width: 20px;
@@ -176,15 +181,41 @@ input[type="range"]::-webkit-slider-thumb {
cursor: pointer;
transition: all ease 0.3s;
margin-left: 5px;
+ border: none;
}
-input[type="range"]::-webkit-slider-thumb:hover {
+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 {
+ @include thumbHover();
+}
+
+input[type="range"]::-moz-range-thumb:hover {
+ @include thumbHover();
+}
+
+input[type="range"]::-ms-thumb:hover {
+ @include thumbHover();
+}
+
input[type="number"],
-input[type="text"] {
+input[type="text"],
+input[type="password"] {
appearance: none;
border-radius: 10px;
border: var(--border-in-light);
@@ -194,6 +225,7 @@ input[type="text"] {
color: var(--black);
padding: 0 10px;
max-width: 50%;
+ font-family: inherit;
}
div.math {
@@ -260,10 +292,6 @@ pre {
.clickable {
cursor: pointer;
- div:not(.no-dark) > svg {
- filter: invert(0.5);
- }
-
&:hover {
filter: brightness(0.9);
}
diff --git a/app/utils.ts b/app/utils.ts
index 5fe277c63..0e4a8eaea 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -1,22 +1,28 @@
+import { EmojiStyle } from "emoji-picker-react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
export function trimTopic(topic: string) {
- return topic.replace(/[,。!?、,.!?]*$/, "");
+ return topic.replace(/[,。!?”“"、,.!?]*$/, "");
}
export async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
- } catch (error) {
- const textarea = document.createElement("textarea");
- textarea.value = text;
- document.body.appendChild(textarea);
- textarea.select();
- document.execCommand("copy");
- document.body.removeChild(textarea);
- } finally {
showToast(Locale.Copy.Success);
+ } catch (error) {
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand("copy");
+ showToast(Locale.Copy.Success);
+ } catch (error) {
+ showToast(Locale.Copy.Failed);
+ }
+ document.body.removeChild(textArea);
}
}
@@ -45,6 +51,12 @@ export function isMobileScreen() {
return window.innerWidth <= 600;
}
+export function isFirefox() {
+ return (
+ typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent)
+ );
+}
+
export function selectOrCopy(el: HTMLElement, content: string) {
const currentSelection = window.getSelection();
@@ -57,27 +69,58 @@ export function selectOrCopy(el: HTMLElement, content: string) {
return true;
}
-export function queryMeta(key: string, defaultValue?: string): string {
- let ret: string;
- if (document) {
- const meta = document.head.querySelector(
- `meta[name='${key}']`,
- ) as HTMLMetaElement;
- ret = meta?.content ?? "";
- } else {
- ret = defaultValue ?? "";
- }
-
- return ret;
+export function getEmojiUrl(unified: string, style: EmojiStyle) {
+ return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}
-let currentId: string;
-export function getCurrentVersion() {
- if (currentId) {
- return currentId;
+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);
}
- currentId = queryMeta("version");
-
- return currentId;
+ 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;
+}
+
+export function getCSSVar(varName: string) {
+ return getComputedStyle(document.body).getPropertyValue(varName).trim();
}
diff --git a/docs/faq-cn.md b/docs/faq-cn.md
new file mode 100644
index 000000000..6251889ca
--- /dev/null
+++ b/docs/faq-cn.md
@@ -0,0 +1,165 @@
+# 常见问题
+
+## 如何快速获得帮助?
+1. 询问ChatGPT / Bing / 百度 / Google等。
+2. 询问网友。请提供问题的背景信息和碰到问题的详细描述。高质量的提问容易获得有用的答案。
+
+# 部署相关问题
+
+各种部署方式详细教程参考:https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b
+
+## 为什么 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 的控制台页面;
+- 选中你的 chatgpt next web 项目;
+- 点击页面头部的 Settings 选项;
+- 找到侧边栏的 Environment Variables 选项;
+- 修改对应的值即可。
+
+## 环境变量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)
+
+如果你使用 ngnix 反向代理,需要在配置文件中增加下列代码:
+```
+# 不缓存,支持流式输出
+proxy_cache off; # 关闭缓存
+proxy_buffering off; # 关闭代理缓冲
+chunked_transfer_encoding on; # 开启分块传输编码
+tcp_nopush on; # 开启TCP NOPUSH选项,禁止Nagle算法
+tcp_nodelay on; # 开启TCP NODELAY选项,禁止延迟ACK算法
+keepalive_timeout 300; # 设定keep-alive超时时间为65秒
+```
+
+如果你是在 netlify 部署,此问题依然等待解决,请耐心等待。
+
+## 我部署好了,但是无法访问
+请检查排除以下问题:
+- 服务启动了吗?
+- 端口正确映射了吗?
+- 防火墙开放端口了吗?
+- 到服务器的路由通吗?
+- 域名正确解析了吗?
+
+## 什么是代理,如何使用?
+由于OpenAI的IP限制,中国和其他一些国家/地区无法直接连接OpenAI API,需要通过代理。你可以使用代理服务器(正向代理),或者已经设置好的OpenAI API反向代理。
+- 正向代理例子:科学上网梯子。docker部署的情况下,设置环境变量HTTP_PROXY为你的代理地址(例如:10.10.10.10:8002)。
+- 反向代理例子:可以用别人搭建的代理地址,或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。
+
+## 国内服务器可以部署吗?
+可以但需要解决的问题:
+- 需要代理才能连接github和openAI等网站;
+- 国内服务器要设置域名解析的话需要备案;
+- 国内政策限制代理访问外网/ChatGPT相关应用,可能被封。
+
+
+# 使用相关问题
+
+## 为什么会一直提示“出错了,稍后重试吧”
+原因可能有很多,请依次排查:
+- 请先检查你的代码版本是否为最新版本,更新到最新版本后重试;
+- 请检查 api key 是否设置正确,环境变量名称必须为全大写加下划线;
+- 请检查 api key 是否可用;
+- 如果经历了上述步骤依旧无法确定问题,请在 issue 区提交一个新 issue,并附上 vercel 的 runtime log 或者 docker 运行时的 log。
+
+## 为什么 ChatGPT 的回复会乱码
+设置界面 - 模型设置项中,有一项为 `temperature`,如果此值大于 1,那么就有可能造成回复乱码,将其调回 1 以内即可。
+
+## 使用时提示“现在是未授权状态,请在设置页输入访问密码”?
+项目通过环境变量CODE设置了访问密码。第一次使用时,需要到设置中,输入访问码才可以使用。
+
+## 使用时提示"You exceeded your current quota, ..."
+API KEY有问题。余额不足。
+
+# 网络服务相关问题
+## Cloudflare是什么?
+Cloudflare(CF)是一个提供CDN,域名管理,静态页面托管,边缘计算函数部署等的网络服务供应商。常见的用途:购买和/或托管你的域名(解析、动态域名等),给你的服务器套上CDN(可以隐藏ip免被墙),部署网站(CF Pages)。CF免费提供大多数服务。
+
+## Vercel是什么?
+Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建和部署现代 Web 应用程序。本项目以及许多Web应用可以一键免费部署在Vercel上。无需懂代码,无需懂linux,无需服务器,无需付费,无需设置OpenAI API代理。缺点是需要绑定域名才可以在国内无墙访问。
+
+## 如何获得一个域名?
+1. 自己去域名供应商处注册,国外有Namesilo(支持支付宝), Cloudflare等等,国内有万网等等;
+2. 免费的域名供应商:eu.org(二级域名)等;
+3. 问朋友要一个免费的二级域名。
+
+## 如何获得一台服务器
+- 国外服务器供应商举例:亚马逊云,谷歌云,Vultr,Bandwagon,Hostdare,等等;
+国外服务器事项:服务器线路影响国内访问速度,推荐CN2 GIA和CN2线路的服务器。若服务器在国内访问困难(丢包严重等),可以尝试套CDN(Cloudflare等供应商)。
+- 国内服务器供应商:阿里云,腾讯等;
+国内服务器事项:解析域名需要备案;国内服务器带宽较贵;访问国外网站(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访问?
+- GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist(准备好你的OpenAI组织ID):https://openai.com/waitlist/gpt-4-api
+之后等待邮件消息。
+- 开通 ChatGPT Plus 不代表有 GPT-4 权限,两者毫无关系。
+
+## 如何使用 Azure OpenAI 接口
+请参考:[#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 爆破记录,那么就是密码被爆破了。
+
+## API是怎么计费的?
+OpenAI网站计费说明:https://openai.com/pricing#language-models
+OpenAI根据token数收费,1000个token通常可代表750个英文单词,或500个汉字。输入(Prompt)和输出(Completion)分别统计费用。
+|模型|用户输入(Prompt)计费|模型输出(Completion)计费|每次交互最大token数|
+|----|----|----|----|
+|gpt-3.5|$0.002 / 1千tokens|$0.002 / 1千tokens|4096|
+|gpt-4|$0.03 / 1千tokens|$0.06 / 1千tokens|8192|
+|gpt-4-32K|$0.06 / 1千tokens|$0.12 / 1千tokens|32768|
+
+## gpt-3.5-turbo和gpt3.5-turbo-0301(或者gpt3.5-turbo-mmdd)模型有什么区别?
+官方文档说明:https://platform.openai.com/docs/models/gpt-3-5
+- gpt-3.5-turbo是最新的模型,会不断得到更新。
+- gpt-3.5-turbo-0301是3月1日定格的模型快照,不会变化,预期3个月后被新快照替代。
diff --git a/docs/faq-en.md b/docs/faq-en.md
new file mode 100644
index 000000000..319fc7dea
--- /dev/null
+++ b/docs/faq-en.md
@@ -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.
diff --git a/docs/images/cover.png b/docs/images/cover.png
new file mode 100644
index 000000000..20fbf6a86
Binary files /dev/null and b/docs/images/cover.png differ
diff --git a/docs/images/enable-actions-sync.jpg b/docs/images/enable-actions-sync.jpg
new file mode 100644
index 000000000..4a69da928
Binary files /dev/null and b/docs/images/enable-actions-sync.jpg differ
diff --git a/docs/images/enable-actions.jpg b/docs/images/enable-actions.jpg
new file mode 100644
index 000000000..a4f4f0f1f
Binary files /dev/null and b/docs/images/enable-actions.jpg differ
diff --git a/static/icon.svg b/docs/images/icon.svg
similarity index 100%
rename from static/icon.svg
rename to docs/images/icon.svg
diff --git a/docs/images/more.png b/docs/images/more.png
new file mode 100644
index 000000000..dfdb3580d
Binary files /dev/null and b/docs/images/more.png differ
diff --git a/docs/images/settings.png b/docs/images/settings.png
new file mode 100644
index 000000000..e1cdd07ee
Binary files /dev/null and b/docs/images/settings.png differ
diff --git a/docs/images/vercel/vercel-create-1.jpg b/docs/images/vercel/vercel-create-1.jpg
new file mode 100644
index 000000000..f0bbd0028
Binary files /dev/null and b/docs/images/vercel/vercel-create-1.jpg differ
diff --git a/docs/images/vercel/vercel-create-2.jpg b/docs/images/vercel/vercel-create-2.jpg
new file mode 100644
index 000000000..157768a88
Binary files /dev/null and b/docs/images/vercel/vercel-create-2.jpg differ
diff --git a/docs/images/vercel/vercel-create-3.jpg b/docs/images/vercel/vercel-create-3.jpg
new file mode 100644
index 000000000..2eaae1f9f
Binary files /dev/null and b/docs/images/vercel/vercel-create-3.jpg differ
diff --git a/docs/images/vercel/vercel-env-edit.jpg b/docs/images/vercel/vercel-env-edit.jpg
new file mode 100644
index 000000000..5b115935e
Binary files /dev/null and b/docs/images/vercel/vercel-env-edit.jpg differ
diff --git a/docs/images/vercel/vercel-redeploy.jpg b/docs/images/vercel/vercel-redeploy.jpg
new file mode 100644
index 000000000..ee3483fa7
Binary files /dev/null and b/docs/images/vercel/vercel-redeploy.jpg differ
diff --git a/docs/vercel-cn.md b/docs/vercel-cn.md
new file mode 100644
index 000000000..c49229694
--- /dev/null
+++ b/docs/vercel-cn.md
@@ -0,0 +1,39 @@
+# Vercel 的使用说明
+
+## 如何新建项目
+当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。
+
+
+1. 进入 Vercel 控制台首页;
+2. 点击 Add New;
+3. 选择 Project。
+
+
+1. 在 Import Git Repository 处,搜索 chatgpt-next-web;
+2. 选中新 fork 的项目,点击 Import。
+
+
+1. 在项目配置页,点开 Environmane Variables 开始配置环境变量;
+2. 依次新增名为 OPENAI_API_KEY 和 CODE 的环境变量;
+3. 填入环境变量对应的值;
+4. 点击 Add 确认增加环境变量;
+5. 请确保你添加了 OPENAI_API_KEY,否则无法使用;
+6. 点击 Deploy,创建完成,耐心等待 5 分钟左右部署完成。
+
+## 如何增加自定义域名
+[TODO]
+
+## 如何更改环境变量
+
+1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮;
+2. 点击左侧的 Environment Variables;
+3. 点击已有条目的右侧按钮;
+4. 选择 Edit 进行编辑,然后保存即可。
+
+⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效!
+
+## 如何重新部署
+
+1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮;
+2. 选择列表最顶部一条的右侧按钮;
+3. 点击 Redeploy 即可重新部署。
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index 9338a2c6b..d16a812d9 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,21 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
-import { ACCESS_CODES } from "./app/api/access";
+import { getServerSideConfig } from "./app/config/server";
import md5 from "spark-md5";
export const config = {
matcher: ["/api/openai", "/api/chat-stream"],
};
+const serverConfig = getServerSideConfig();
+
+function getIP(req: NextRequest) {
+ let ip = req.ip ?? req.headers.get("x-real-ip");
+ const forwardedFor = req.headers.get("x-forwarded-for");
+
+ if (!ip && forwardedFor) {
+ ip = forwardedFor.split(",").at(0) ?? "";
+ }
+
+ return ip;
+}
+
export function middleware(req: NextRequest) {
const accessCode = req.headers.get("access-code");
const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim();
- console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
+ console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
+ console.log("[User IP] ", getIP(req));
+ console.log("[Time] ", new Date().toLocaleString());
- if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
+ if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return NextResponse.json(
{
error: true,
@@ -30,7 +45,7 @@ export function middleware(req: NextRequest) {
// inject api key
if (!token) {
- const apiKey = process.env.OPENAI_API_KEY;
+ const apiKey = serverConfig.apiKey;
if (apiKey) {
console.log("[Auth] set system token");
req.headers.set("token", apiKey);
diff --git a/next.config.js b/next.config.js
index fc164db9c..f7d5ff086 100644
--- a/next.config.js
+++ b/next.config.js
@@ -8,14 +8,11 @@ const nextConfig = {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
- }); // 针对 SVG 的处理规则
+ });
return config;
- }
+ },
+ output: "standalone",
};
-if (process.env.DOCKER) {
- nextConfig.output = 'standalone'
-}
-
module.exports = nextConfig;
diff --git a/package.json b/package.json
index 18e227f1a..19047ad11 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "chatgpt-next-web",
- "version": "1.1",
+ "version": "1.9.3",
"private": false,
"license": "Anti 996",
"scripts": {
@@ -9,15 +9,17 @@
"start": "next start",
"lint": "next lint",
"fetch": "node ./scripts/fetch-prompts.mjs",
- "prepare": "husky install"
+ "prepare": "husky install",
+ "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
},
"dependencies": {
+ "@hello-pangea/dnd": "^16.2.0",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7",
"eventsource-parser": "^0.1.0",
"fuse.js": "^6.6.2",
- "next": "^13.2.3",
+ "next": "^13.3.1-canary.8",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",
diff --git a/scripts/.gitignore b/scripts/.gitignore
new file mode 100644
index 000000000..80fe56c37
--- /dev/null
+++ b/scripts/.gitignore
@@ -0,0 +1 @@
+proxychains.conf
diff --git a/scripts/fetch-prompts.mjs b/scripts/fetch-prompts.mjs
index 3c4801443..7f6818d3b 100644
--- a/scripts/fetch-prompts.mjs
+++ b/scripts/fetch-prompts.mjs
@@ -1,14 +1,13 @@
import fetch from "node-fetch";
import fs from "fs/promises";
-const RAW_CN_URL =
- "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
-const CN_URL =
- "https://cdn.jsdelivr.net/gh/PlexPt/awesome-chatgpt-prompts-zh@main/prompts-zh.json";
-const RAW_EN_URL =
- "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
-const EN_URL =
- "https://cdn.jsdelivr.net/gh/f/awesome-chatgpt-prompts@main/prompts.csv";
+const RAW_FILE_URL = "https://raw.githubusercontent.com/";
+const MIRRORF_FILE_URL = "https://raw.fgit.ml/";
+
+const RAW_CN_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
+const CN_URL = MIRRORF_FILE_URL + RAW_CN_URL;
+const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv";
+const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL;
const FILE = "./public/prompts.json";
async function fetchCN() {
diff --git a/scripts/init-proxy.sh b/scripts/init-proxy.sh
new file mode 100644
index 000000000..acba064f4
--- /dev/null
+++ b/scripts/init-proxy.sh
@@ -0,0 +1,5 @@
+dir="$(dirname "$0")"
+config=$dir/proxychains.conf
+host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //')
+cp $dir/proxychains.template.conf $config
+sed -i "\$s/.*/http $host_ip 7890/" $config
diff --git a/scripts/proxychains.template.conf b/scripts/proxychains.template.conf
new file mode 100644
index 000000000..e78b96a68
--- /dev/null
+++ b/scripts/proxychains.template.conf
@@ -0,0 +1,12 @@
+strict_chain
+proxy_dns
+
+remote_dns_subnet 224
+
+tcp_read_time_out 15000
+tcp_connect_time_out 8000
+
+localnet 127.0.0.0/255.0.0.0
+
+[ProxyList]
+socks4 127.0.0.1 9050
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 63a28bf09..b96533398 100644
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -61,4 +61,5 @@ read -p "Enter CODE: " CODE
read -p "Enter PORT: " PORT
# Build and run the project using the environment variables
-OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build && OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start
+OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn build
+OPENAI_API_KEY=$OPENAI_API_KEY CODE=$CODE PORT=$PORT yarn start
diff --git a/static/cover.png b/static/cover.png
deleted file mode 100644
index 0587bd29a..000000000
Binary files a/static/cover.png and /dev/null differ
diff --git a/static/more.png b/static/more.png
deleted file mode 100644
index 683a16f07..000000000
Binary files a/static/more.png and /dev/null differ
diff --git a/static/settings.png b/static/settings.png
deleted file mode 100644
index 22d841c31..000000000
Binary files a/static/settings.png and /dev/null differ
diff --git a/tsconfig.json b/tsconfig.json
index 14d189328..c73eef3e8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"]
}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 000000000..0cae358a1
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,5 @@
+{
+ "github": {
+ "silent": true
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 9a937276c..342ea4a44 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -954,7 +954,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
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"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
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"
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":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -1086,10 +1099,10 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
-"@next/env@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-13.2.4.tgz#8b763700262b2445140a44a8c8d088cef676dbae"
- integrity sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==
+"@next/env@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1-canary.8.tgz#9f5cf57999e4f4b59ef6407924803a247cc4e451"
+ integrity sha512-xZfNu7yq3OfiC4rkGuGMcqb25se+ZHRqajSdny8dp+nZzkNSK1SHuNT3W8faI+KGk6dqzO/zAdHR9YrqnQlCAg==
"@next/eslint-plugin-next@13.2.3":
version "13.2.3"
@@ -1098,70 +1111,50 @@
dependencies:
glob "7.1.7"
-"@next/swc-android-arm-eabi@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz#758d0403771e549f9cee71cbabc0cb16a6c947c0"
- integrity sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==
+"@next/swc-darwin-arm64@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1-canary.8.tgz#66786ba76d37c210c184739624c6f84eaf2dc52b"
+ integrity sha512-BLbvhcaSzwuXbREOmJiqAdXVD7Jl9830hDY5ZTTNg7hXqEZgoMg2LxAEmtaaBMVZRfDQjd5bH3QPBV8fbG4UKg==
-"@next/swc-android-arm64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz#834d586523045110d5602e0c8aae9028835ac427"
- integrity sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==
+"@next/swc-darwin-x64@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1-canary.8.tgz#289296bd3cc55db7fef42037eb89ce4a6260ba31"
+ integrity sha512-n4tJKPIvFTZshS1TVWrsqaW7h9VW+BmguO/AlZ3Q3NJ9hWxC5L4lxn2T6CTQ4M30Gf+t5u+dPzYLQ5IDtJFnFQ==
-"@next/swc-darwin-arm64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz#5006fca179a36ef3a24d293abadec7438dbb48c6"
- integrity sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==
+"@next/swc-linux-arm64-gnu@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1-canary.8.tgz#dc79e8005849b6482241b460abdce9334665c766"
+ integrity sha512-AxnsgZ56whwVAeejyEZMk8xc8Vapwzb3Zn0YdZzPCR42WKfkcSkM+AWfq33zUOZnjvCmQBDyfHIo4CURVweR6g==
-"@next/swc-darwin-x64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz#6549c7c04322766acc3264ccdb3e1b43fcaf7946"
- integrity sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==
+"@next/swc-linux-arm64-musl@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1-canary.8.tgz#f70873add4aad7ced36f760d1640adc008b7dc03"
+ integrity sha512-zc7rzhtrHMWZ/phvjCNplHGo+ZLembjtluI5J8Xl4iwQQCyZwAtnmQhs37/zkdi6dHZou+wcFBZWRz14awRDBw==
-"@next/swc-freebsd-x64@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz#0bbe28979e3e868debc2cc06e45e186ce195b7f4"
- integrity sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==
+"@next/swc-linux-x64-gnu@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1-canary.8.tgz#fe81b8033628c6cf74e154f2db8c8c7f1593008f"
+ integrity sha512-vNbFDiuZ9fWmcznlilDbflZLb04evWPUQlyDT7Tqjd964PlSIaaX3tr64pdYjJOljDaqTr2Kbx0YW74mWF/PEw==
-"@next/swc-linux-arm-gnueabihf@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz#1d28d2203f5a7427d6e7119d7bcb5fc40959fb3e"
- integrity sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==
+"@next/swc-linux-x64-musl@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1-canary.8.tgz#ada4585046a7937f96f2d39fc4aaca12826dde5f"
+ integrity sha512-/FVBPJEBDZYCNraocRWtd5ObAgNi9VFnzJYGYDYIj4jKkFRWWm/CaWu9A7toQACC/JDy262uPyDPathXT9BAqQ==
-"@next/swc-linux-arm64-gnu@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz#eb26448190948cdf4c44b8f34110a3ecea32f1d0"
- integrity sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==
+"@next/swc-win32-arm64-msvc@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1-canary.8.tgz#21b4f6c4be61845759753df9313bd9bcbb241969"
+ integrity sha512-8jMwRCeI26yVZLPwG0AjOi4b1yqSeqYmbHA7r+dqiV0OgFdYjnbyHU1FmiKDaC5SnnJN6LWV2Qjer9GDD0Kcuw==
-"@next/swc-linux-arm64-musl@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz#c4227c0acd94a420bb14924820710e6284d234d3"
- integrity sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==
+"@next/swc-win32-ia32-msvc@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1-canary.8.tgz#e23192e1d1b1a32b0eb805363b02360c5b523a77"
+ integrity sha512-kcYB9iSEikFhv0I9uQDdgQ2lm8i3O8LA+GhnED9e5VtURBwOSwED7c6ZpaRQBYSPgnEA9/xiJVChICE/I7Ig1g==
-"@next/swc-linux-x64-gnu@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz#6bcb540944ee9b0209b33bfc23b240c2044dfc3e"
- integrity sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==
-
-"@next/swc-linux-x64-musl@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz#ce21e43251eaf09a09df39372b2c3e38028c30ff"
- integrity sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==
-
-"@next/swc-win32-arm64-msvc@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz#68220063d8e5e082f5465498675640dedb670ff1"
- integrity sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==
-
-"@next/swc-win32-ia32-msvc@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz#7c120ab54a081be9566df310bed834f168252990"
- integrity sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==
-
-"@next/swc-win32-x64-msvc@13.2.4":
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz#5abda92fe12b9829bf7951c4a221282c56041144"
- integrity sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==
+"@next/swc-win32-x64-msvc@13.3.1-canary.8":
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1-canary.8.tgz#a3f29404955cba2193de5e74fd5d9fcfdcb0ab51"
+ integrity sha512-UKrGHonKVWBNg+HI4J8pXE6Jjjl8GwjhygFau71s8M0+jSy99y5Y+nGH9EmMNWKNvrObukyYvrs6OsAusKdCqw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1333,6 +1326,14 @@
dependencies:
"@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":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -1408,6 +1409,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
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":
version "5.57.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.57.0.tgz#f675bf2cd1a838949fd0de5683834417b757e4fa"
@@ -1704,6 +1710,13 @@ browserslist@^4.21.3, browserslist@^4.21.5:
node-releases "^2.0.8"
update-browserslist-db "^1.0.10"
+busboy@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
+ integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
+ dependencies:
+ streamsearch "^1.1.0"
+
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -1912,6 +1925,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
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:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
@@ -2885,6 +2905,13 @@ highlight.js@~11.7.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
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:
version "4.3.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
@@ -3527,6 +3554,11 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -3892,30 +3924,27 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
-next@^13.2.3:
- version "13.2.4"
- resolved "https://registry.yarnpkg.com/next/-/next-13.2.4.tgz#2363330392b0f7da02ab41301f60857ffa7f67d6"
- integrity sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==
+next@^13.3.1-canary.8:
+ version "13.3.1-canary.8"
+ resolved "https://registry.yarnpkg.com/next/-/next-13.3.1-canary.8.tgz#f0846e5eada1491884326786a0749d5adc04c24d"
+ integrity sha512-z4QUgyAN+hSWSEqb4pvGvC3iRktE6NH2DVLU4AvfqNYpzP+prePiJC8HN/cJpFhGW9YbhyRLi5FliDC631OOag==
dependencies:
- "@next/env" "13.2.4"
+ "@next/env" "13.3.1-canary.8"
"@swc/helpers" "0.4.14"
+ busboy "1.6.0"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.1.1"
optionalDependencies:
- "@next/swc-android-arm-eabi" "13.2.4"
- "@next/swc-android-arm64" "13.2.4"
- "@next/swc-darwin-arm64" "13.2.4"
- "@next/swc-darwin-x64" "13.2.4"
- "@next/swc-freebsd-x64" "13.2.4"
- "@next/swc-linux-arm-gnueabihf" "13.2.4"
- "@next/swc-linux-arm64-gnu" "13.2.4"
- "@next/swc-linux-arm64-musl" "13.2.4"
- "@next/swc-linux-x64-gnu" "13.2.4"
- "@next/swc-linux-x64-musl" "13.2.4"
- "@next/swc-win32-arm64-msvc" "13.2.4"
- "@next/swc-win32-ia32-msvc" "13.2.4"
- "@next/swc-win32-x64-msvc" "13.2.4"
+ "@next/swc-darwin-arm64" "13.3.1-canary.8"
+ "@next/swc-darwin-x64" "13.3.1-canary.8"
+ "@next/swc-linux-arm64-gnu" "13.3.1-canary.8"
+ "@next/swc-linux-arm64-musl" "13.3.1-canary.8"
+ "@next/swc-linux-x64-gnu" "13.3.1-canary.8"
+ "@next/swc-linux-x64-musl" "13.3.1-canary.8"
+ "@next/swc-win32-arm64-msvc" "13.3.1-canary.8"
+ "@next/swc-win32-ia32-msvc" "13.3.1-canary.8"
+ "@next/swc-win32-x64-msvc" "13.3.1-canary.8"
node-domexception@^1.0.0:
version "1.0.0"
@@ -4211,6 +4240,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
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:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@@ -4219,7 +4253,7 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
-react-is@^16.13.1:
+react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -4250,6 +4284,18 @@ react-markdown@^8.0.5:
unist-util-visit "^4.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:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -4264,6 +4310,13 @@ readdirp@~3.6.0:
dependencies:
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:
version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
@@ -4599,6 +4652,11 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
+streamsearch@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
+ integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
+
string-argv@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
@@ -4774,6 +4832,11 @@ tiny-glob@^0.2.9:
globalyzer "0.1.0"
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -4979,7 +5042,12 @@ use-debounce@^9.0.3:
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.3.tgz#bac660c19ab7b38662e08608fee23c7ad303f532"
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"
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==