mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-10 20:16:37 +08:00
Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web into duang/azure_openai
This commit is contained in:
commit
3e904f3fcf
@ -102,7 +102,7 @@ We recommend that you follow the steps below to re-deploy:
|
||||
|
||||
### Enable Automatic Updates
|
||||
|
||||
After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
|
||||
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
|
||||
|
||||

|
||||
|
||||
@ -110,7 +110,7 @@ After forking the project, due to the limitations imposed by Github, you need to
|
||||
|
||||
### Manually Updating Code
|
||||
|
||||
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
|
||||
If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
|
||||
|
||||
You can star or watch this project or follow author to get release notifictions in time.
|
||||
|
||||
@ -146,6 +146,10 @@ Access passsword, separated by comma.
|
||||
|
||||
Override openai api request base url.
|
||||
|
||||
### `OPENAI_ORG_ID` (optional)
|
||||
|
||||
Specify OpenAI organization ID.
|
||||
|
||||
## Development
|
||||
|
||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||
|
@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
|
||||
|
||||
### `OPENAI_ORG_ID` (可选)
|
||||
|
||||
指定 OpenAI 中的组织 ID。
|
||||
|
||||
## 开发
|
||||
|
||||
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
|
||||
|
@ -59,6 +59,4 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
};
|
||||
export const runtime = "experimental-edge";
|
||||
|
@ -24,11 +24,18 @@ export async function requestOpenai(req: NextRequest) {
|
||||
console.log("[Proxy] ", openaiPath);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
if (process.env.OPENAI_ORG_ID) {
|
||||
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
|
||||
}
|
||||
|
||||
return fetch(`${baseUrl}/${openaiPath}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"api-key": apiKey || "",
|
||||
...(process.env.OPENAI_ORG_ID && {
|
||||
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
||||
}),
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
|
@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) {
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,4 @@ export async function GET(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
};
|
||||
export const runtime = "experimental-edge";
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { isMobileScreen } from "../utils";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
@ -21,6 +22,7 @@ export function ChatItem(props: {
|
||||
selected: boolean;
|
||||
id: number;
|
||||
index: number;
|
||||
narrow?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||
@ -34,6 +36,10 @@ export function ChatItem(props: {
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{props.narrow ? (
|
||||
<div className={styles["chat-item-narrow"]}>{props.count}</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles["chat-item-title"]}>{props.title}</div>
|
||||
<div className={styles["chat-item-info"]}>
|
||||
<div className={styles["chat-item-count"]}>
|
||||
@ -41,6 +47,9 @@ export function ChatItem(props: {
|
||||
</div>
|
||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
@ -50,7 +59,7 @@ export function ChatItem(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatList() {
|
||||
export function ChatList(props: { narrow?: boolean }) {
|
||||
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
|
||||
useChatStore((state) => [
|
||||
state.sessions,
|
||||
@ -60,6 +69,7 @@ export function ChatList() {
|
||||
state.moveSession,
|
||||
]);
|
||||
const chatStore = useChatStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
const { destination, source } = result;
|
||||
@ -95,8 +105,16 @@ export function ChatList() {
|
||||
id={item.id}
|
||||
index={i}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => selectSession(i)}
|
||||
onDelete={() => chatStore.deleteSession(i)}
|
||||
onClick={() => {
|
||||
navigate(Path.Chat);
|
||||
selectSession(i);
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
|
||||
chatStore.deleteSession(i);
|
||||
}
|
||||
}}
|
||||
narrow={props.narrow}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
|
@ -10,6 +10,7 @@ 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 BlackBotIcon from "../icons/black-bot.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaxIcon from "../icons/max.svg";
|
||||
@ -30,15 +31,17 @@ import {
|
||||
createMessage,
|
||||
useAccessStore,
|
||||
Theme,
|
||||
ModelType,
|
||||
useAppConfig,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
getEmojiUrl,
|
||||
isMobileScreen,
|
||||
selectOrCopy,
|
||||
autoGrowTextArea,
|
||||
useMobileScreen,
|
||||
} from "../utils";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
@ -52,6 +55,8 @@ import styles from "./home.module.scss";
|
||||
import chatStyle from "./chat.module.scss";
|
||||
|
||||
import { Input, Modal, showModal } from "./ui-lib";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
|
||||
const Markdown = dynamic(
|
||||
async () => memo((await import("./markdown")).Markdown),
|
||||
@ -64,13 +69,17 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
|
||||
export function Avatar(props: { role: Message["role"] }) {
|
||||
const config = useChatStore((state) => state.config);
|
||||
export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
|
||||
const config = useAppConfig();
|
||||
|
||||
if (props.role !== "user") {
|
||||
return (
|
||||
<div className="no-dark">
|
||||
{props.model?.startsWith("gpt-4") ? (
|
||||
<BlackBotIcon className={styles["user-avtar"]} />
|
||||
) : (
|
||||
<BotIcon className={styles["user-avtar"]} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -277,7 +286,7 @@ function PromptToast(props: {
|
||||
}
|
||||
|
||||
function useSubmitHandler() {
|
||||
const config = useChatStore((state) => state.config);
|
||||
const config = useAppConfig();
|
||||
const submitKey = config.submitKey;
|
||||
|
||||
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@ -353,16 +362,16 @@ export function ChatActions(props: {
|
||||
scrollToBottom: () => void;
|
||||
hitBottom: boolean;
|
||||
}) {
|
||||
const chatStore = useChatStore();
|
||||
const config = useAppConfig();
|
||||
|
||||
// switch themes
|
||||
const theme = chatStore.config.theme;
|
||||
const theme = 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));
|
||||
config.update((config) => (config.theme = nextTheme));
|
||||
}
|
||||
|
||||
// stop all responses
|
||||
@ -412,10 +421,7 @@ export function ChatActions(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function Chat(props: {
|
||||
showSideBar?: () => void;
|
||||
sideBarShowing?: boolean;
|
||||
}) {
|
||||
export function Chat() {
|
||||
type RenderMessage = Message & { preview?: boolean };
|
||||
|
||||
const chatStore = useChatStore();
|
||||
@ -423,7 +429,8 @@ export function Chat(props: {
|
||||
state.currentSession(),
|
||||
state.currentSessionIndex,
|
||||
]);
|
||||
const fontSize = useChatStore((state) => state.config.fontSize);
|
||||
const config = useAppConfig();
|
||||
const fontSize = config.fontSize;
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [userInput, setUserInput] = useState("");
|
||||
@ -432,6 +439,8 @@ export function Chat(props: {
|
||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||
const [hitBottom, setHitBottom] = useState(false);
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||
@ -462,7 +471,7 @@ export function Chat(props: {
|
||||
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
||||
const inputRows = Math.min(
|
||||
5,
|
||||
Math.max(2 + Number(!isMobileScreen()), rows),
|
||||
Math.max(2 + Number(!isMobileScreen), rows),
|
||||
);
|
||||
setInputRows(inputRows);
|
||||
},
|
||||
@ -485,7 +494,7 @@ export function Chat(props: {
|
||||
// clear search results
|
||||
if (n === 0) {
|
||||
setPromptHints([]);
|
||||
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||
// check if need to trigger auto completion
|
||||
if (text.startsWith("/")) {
|
||||
let searchText = text.slice(1);
|
||||
@ -502,7 +511,7 @@ export function Chat(props: {
|
||||
setBeforeInput(userInput);
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
if (!isMobileScreen()) inputRef.current?.focus();
|
||||
if (!isMobileScreen) inputRef.current?.focus();
|
||||
setAutoScroll(true);
|
||||
};
|
||||
|
||||
@ -536,7 +545,7 @@ export function Chat(props: {
|
||||
}
|
||||
};
|
||||
|
||||
const findLastUesrIndex = (messageId: number) => {
|
||||
const findLastUserIndex = (messageId: number) => {
|
||||
// find last user input message and resend
|
||||
let lastUserMessageIndex: number | null = null;
|
||||
for (let i = 0; i < session.messages.length; i += 1) {
|
||||
@ -559,14 +568,14 @@ export function Chat(props: {
|
||||
};
|
||||
|
||||
const onDelete = (botMessageId: number) => {
|
||||
const userIndex = findLastUesrIndex(botMessageId);
|
||||
const userIndex = findLastUserIndex(botMessageId);
|
||||
if (userIndex === null) return;
|
||||
deleteMessage(userIndex);
|
||||
};
|
||||
|
||||
const onResend = (botMessageId: number) => {
|
||||
// find last user input message and resend
|
||||
const userIndex = findLastUesrIndex(botMessageId);
|
||||
const userIndex = findLastUserIndex(botMessageId);
|
||||
if (userIndex === null) return;
|
||||
|
||||
setIsLoading(true);
|
||||
@ -576,8 +585,6 @@ export function Chat(props: {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
const context: RenderMessage[] = session.context.slice();
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
@ -634,7 +641,7 @@ export function Chat(props: {
|
||||
|
||||
// Auto focus
|
||||
useEffect(() => {
|
||||
if (props.sideBarShowing && isMobileScreen()) return;
|
||||
if (isMobileScreen) return;
|
||||
inputRef.current?.focus();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@ -659,7 +666,7 @@ export function Chat(props: {
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.ChatList}
|
||||
onClick={props?.showSideBar}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["window-action-button"]}>
|
||||
@ -682,13 +689,13 @@ export function Chat(props: {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isMobileScreen() && (
|
||||
{!isMobileScreen && (
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||
bordered
|
||||
onClick={() => {
|
||||
chatStore.updateConfig(
|
||||
config.update(
|
||||
(config) => (config.tightBorder = !config.tightBorder),
|
||||
);
|
||||
}}
|
||||
@ -717,6 +724,11 @@ export function Chat(props: {
|
||||
>
|
||||
{messages.map((message, i) => {
|
||||
const isUser = message.role === "user";
|
||||
const showActions =
|
||||
!isUser &&
|
||||
i > 0 &&
|
||||
!(message.preview || message.content.length === 0);
|
||||
const showTyping = message.preview || message.streaming;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -727,16 +739,15 @@ export function Chat(props: {
|
||||
>
|
||||
<div className={styles["chat-message-container"]}>
|
||||
<div className={styles["chat-message-avatar"]}>
|
||||
<Avatar role={message.role} />
|
||||
<Avatar role={message.role} model={message.model} />
|
||||
</div>
|
||||
{(message.preview || message.streaming) && (
|
||||
{showTyping && (
|
||||
<div className={styles["chat-message-status"]}>
|
||||
{Locale.Chat.Typing}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
{!isUser &&
|
||||
!(message.preview || message.content.length === 0) && (
|
||||
{showActions && (
|
||||
<div className={styles["chat-message-top-actions"]}>
|
||||
{message.streaming ? (
|
||||
<div
|
||||
@ -778,7 +789,7 @@ export function Chat(props: {
|
||||
}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen()) return;
|
||||
if (!isMobileScreen) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
@ -819,7 +830,7 @@ export function Chat(props: {
|
||||
setAutoScroll(false);
|
||||
setTimeout(() => setPromptHints([]), 500);
|
||||
}}
|
||||
autoFocus={!props?.sideBarShowing}
|
||||
autoFocus
|
||||
rows={inputRows}
|
||||
/>
|
||||
<IconButton
|
||||
|
@ -50,7 +50,7 @@
|
||||
flex-direction: column;
|
||||
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
transition: width ease 0.1s;
|
||||
transition: width ease 0.05s;
|
||||
}
|
||||
|
||||
.sidebar-drag {
|
||||
@ -126,11 +126,13 @@
|
||||
.sidebar-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400px;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-body {
|
||||
@ -171,6 +173,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.chat-item-delete {
|
||||
@ -197,6 +200,7 @@
|
||||
color: rgb(166, 166, 166);
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
|
||||
.chat-item-count,
|
||||
@ -206,6 +210,69 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.narrow-sidebar {
|
||||
.sidebar-title,
|
||||
.sidebar-sub-title {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-logo {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
padding: 0;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all ease 0.3s;
|
||||
|
||||
&:hover {
|
||||
.chat-item-narrow {
|
||||
transform: scale(0.7) translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item-narrow {
|
||||
font-weight: bolder;
|
||||
font-size: 24px;
|
||||
line-height: 0;
|
||||
font-weight: lighter;
|
||||
color: var(--black);
|
||||
transform: translateX(0);
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0.1;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.chat-item-delete {
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.sidebar-tail {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.sidebar-actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.sidebar-action {
|
||||
margin-right: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-tail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -246,6 +313,10 @@
|
||||
.chat-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&:last-child {
|
||||
animation: slide-in ease 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user {
|
||||
@ -258,7 +329,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
&:hover {
|
||||
.chat-message-top-actions {
|
||||
|
@ -2,32 +2,32 @@
|
||||
|
||||
require("../polyfill");
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect, StyleHTMLAttributes } from "react";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
import SettingsIcon from "../icons/settings.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
|
||||
import { useChatStore } from "../store";
|
||||
import { getCSSVar, isMobileScreen } from "../utils";
|
||||
import Locale from "../locales";
|
||||
import { getCSSVar, useMobileScreen } from "../utils";
|
||||
import { Chat } from "./chat";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { REPO_URL } from "../constant";
|
||||
import { Path } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import { SideBar } from "./sidebar";
|
||||
import { useAppConfig } from "../store/config";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
<div className={styles["loading-content"]}>
|
||||
<div className={styles["loading-content"] + " no-dark"}>
|
||||
{!props.noLogo && <BotIcon />}
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
@ -38,12 +38,8 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
function useSwitchTheme() {
|
||||
const config = useChatStore((state) => state.config);
|
||||
export function useSwitchTheme() {
|
||||
const config = useAppConfig();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.remove("light");
|
||||
@ -73,53 +69,6 @@ function useSwitchTheme() {
|
||||
}, [config.theme]);
|
||||
}
|
||||
|
||||
function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(500, Math.max(220, x));
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 100) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = chatStore.config.sidebarWidth ?? 300;
|
||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
||||
});
|
||||
|
||||
const onDragMouseDown = (e: MouseEvent) => {
|
||||
startX.current = e.clientX;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobileScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
|
||||
);
|
||||
}, [chatStore.config.sidebarWidth]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
};
|
||||
}
|
||||
|
||||
const useHasHydrated = () => {
|
||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||
|
||||
@ -130,129 +79,58 @@ const useHasHydrated = () => {
|
||||
return hasHydrated;
|
||||
};
|
||||
|
||||
function _Home() {
|
||||
const [createNewSession, currentIndex, removeSession] = useChatStore(
|
||||
(state) => [
|
||||
state.newSession,
|
||||
state.currentSessionIndex,
|
||||
state.removeSession,
|
||||
],
|
||||
);
|
||||
const chatStore = useChatStore();
|
||||
const loading = !useHasHydrated();
|
||||
const [showSideBar, setShowSideBar] = useState(true);
|
||||
|
||||
// setting
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown } = useDragSideBar();
|
||||
|
||||
useSwitchTheme();
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
function WideScreen() {
|
||||
const config = useAppConfig();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
config.tightBorder && !isMobileScreen()
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
config.tightBorder ? styles["tight-container"] : styles.container
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
|
||||
>
|
||||
<div className={styles["sidebar-header"]}>
|
||||
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
</div>
|
||||
<div className={styles["sidebar-logo"]}>
|
||||
<ChatGptIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-body"]}
|
||||
onClick={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
>
|
||||
<ChatList />
|
||||
</div>
|
||||
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={chatStore.deleteSession}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
onClick={() => {
|
||||
setOpenSettings(true);
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={Locale.Home.NewChat}
|
||||
onClick={() => {
|
||||
createNewSession();
|
||||
setShowSideBar(false);
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
</div>
|
||||
<SideBar />
|
||||
|
||||
<div className={styles["window-content"]}>
|
||||
{openSettings ? (
|
||||
<Settings
|
||||
closeSettings={() => {
|
||||
setOpenSettings(false);
|
||||
setShowSideBar(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Chat
|
||||
key="chat"
|
||||
showSideBar={() => setShowSideBar(true)}
|
||||
sideBarShowing={showSideBar}
|
||||
/>
|
||||
)}
|
||||
<Routes>
|
||||
<Route path={Path.Home} element={<Chat />} />
|
||||
<Route path={Path.Chat} element={<Chat />} />
|
||||
<Route path={Path.Settings} element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileScreen() {
|
||||
const location = useLocation();
|
||||
const isHome = location.pathname === Path.Home;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||
|
||||
<div className={styles["window-content"]}>
|
||||
<Routes>
|
||||
<Route path={Path.Home} element={null} />
|
||||
<Route path={Path.Chat} element={<Chat />} />
|
||||
<Route path={Path.Settings} element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
const isMobileScreen = useMobileScreen();
|
||||
useSwitchTheme();
|
||||
|
||||
if (!useHasHydrated()) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<_Home></_Home>
|
||||
<Router>{isMobileScreen ? <MobileScreen /> : <WideScreen />}</Router>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
@ -23,16 +23,18 @@ import {
|
||||
useUpdateStore,
|
||||
useAccessStore,
|
||||
ModalConfigValidator,
|
||||
useAppConfig,
|
||||
} from "../store";
|
||||
import { Avatar } from "./chat";
|
||||
|
||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||
import { copyToClipboard, getEmojiUrl } from "../utils";
|
||||
import Link from "next/link";
|
||||
import { UPDATE_URL } from "../constant";
|
||||
import { Path, UPDATE_URL } from "../constant";
|
||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||
import { ErrorBoundary } from "./error";
|
||||
import { InputRange } from "./input-range";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function UserPromptModal(props: { onClose?: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@ -176,13 +178,13 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Settings(props: { closeSettings: () => void }) {
|
||||
export function Settings() {
|
||||
const navigate = useNavigate();
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
|
||||
useChatStore((state) => [
|
||||
state.config,
|
||||
state.updateConfig,
|
||||
state.resetConfig,
|
||||
const config = useAppConfig();
|
||||
const updateConfig = config.update;
|
||||
const resetConfig = config.reset;
|
||||
const [clearAllData, clearSessions] = useChatStore((state) => [
|
||||
state.clearAllData,
|
||||
state.clearSessions,
|
||||
]);
|
||||
@ -235,7 +237,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
useEffect(() => {
|
||||
const keydownEvent = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
props.closeSettings();
|
||||
navigate(Path.Home);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", keydownEvent);
|
||||
@ -290,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
<div className={styles["window-action-button"]}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={props.closeSettings}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
bordered
|
||||
title={Locale.Settings.Actions.Close}
|
||||
/>
|
||||
@ -671,7 +673,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
value={config.modelConfig.presence_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.5"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
|
150
app/components/sidebar.tsx
Normal file
150
app/components/sidebar.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import SettingsIcon from "../icons/settings.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import Locale from "../locales";
|
||||
|
||||
import { useAppConfig, useChatStore } from "../store";
|
||||
|
||||
import {
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
REPO_URL,
|
||||
} from "../constant";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => null,
|
||||
});
|
||||
|
||||
function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
||||
|
||||
const config = useAppConfig();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 50) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = config.sidebarWidth ?? 300;
|
||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
||||
});
|
||||
|
||||
const onDragMouseDown = (e: MouseEvent) => {
|
||||
startX.current = e.clientX;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
};
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldNarrow =
|
||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||
|
||||
useEffect(() => {
|
||||
const barWidth = shouldNarrow
|
||||
? NARROW_SIDEBAR_WIDTH
|
||||
: limit(config.sidebarWidth ?? 300);
|
||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
shouldNarrow,
|
||||
};
|
||||
}
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
>
|
||||
<div className={styles["sidebar-header"]}>
|
||||
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
</div>
|
||||
<div className={styles["sidebar-logo"]}>
|
||||
<ChatGptIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-body"]}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
navigate(Path.Home);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChatList narrow={shouldNarrow} />
|
||||
</div>
|
||||
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
onClick={chatStore.deleteSession}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<Link to={Path.Settings}>
|
||||
<IconButton icon={<SettingsIcon />} shadow />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
||||
onClick={() => {
|
||||
chatStore.newSession();
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2,7 +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";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
export function Popover(props: {
|
||||
children: JSX.Element;
|
||||
@ -64,6 +64,21 @@ interface ModalProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function Modal(props: ModalProps) {
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
props.onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles["modal-container"]}>
|
||||
<div className={styles["modal-header"]}>
|
||||
|
@ -6,3 +6,13 @@ 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";
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
Chat = "/chat",
|
||||
Settings = "/settings",
|
||||
}
|
||||
|
||||
export const MAX_SIDEBAR_WIDTH = 500;
|
||||
export const MIN_SIDEBAR_WIDTH = 230;
|
||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||
|
28
app/icons/black-bot.svg
Normal file
28
app/icons/black-bot.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30"
|
||||
height="30" viewBox="0 0 30 30" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="29.999999999999996" height="29.999999999999996" />
|
||||
<rect id="path_1" x="0" y="0" width="20.45454545454545" height="20.45454545454545" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)">
|
||||
<rect fill="#E7F8FF" opacity="1"
|
||||
transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)" x="0" y="0"
|
||||
width="29.999999999999996" height="29.999999999999996" rx="10" />
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<g opacity="1"
|
||||
transform="translate(4.772727272727272 4.772727272727273) rotate(0 10.227272727272725 10.227272727272725)">
|
||||
<mask id="bg-mask-1" fill="white">
|
||||
<use xlink:href="#path_1"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-1)">
|
||||
<path id="分组 1" fill-rule="evenodd" style="fill:#000000"
|
||||
transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)" opacity="1"
|
||||
d="M19.11 8.37L19.11 8.37C19.28 7.85 19.37 7.31 19.37 6.76C19.37 5.86 19.13 4.97 18.66 4.19C17.73 2.59 16 1.6 14.13 1.6C13.76 1.6 13.4 1.64 13.04 1.71C12.06 0.62 10.65 0 9.17 0L9.14 0L9.13 0C6.86 0 4.86 1.44 4.16 3.57C2.7 3.86 1.44 4.76 0.71 6.04C0.24 6.83 0 7.72 0 8.63C0 9.9 0.48 11.14 1.35 12.08C1.17 12.6 1.08 13.15 1.08 13.69C1.08 14.6 1.33 15.49 1.79 16.27C2.92 18.21 5.2 19.21 7.42 18.74C8.4 19.83 9.8 20.45 11.28 20.45L11.31 20.45L11.33 20.45C13.59 20.45 15.6 19.01 16.3 16.88C17.76 16.59 19.01 15.69 19.75 14.41C20.21 13.63 20.45 12.74 20.45 11.83C20.45 10.55 19.97 9.32 19.11 8.37Z M8.94734 18.1579C8.90734 18.1879 8.86734 18.2079 8.82734 18.2279C9.52734 18.8079 10.3973 19.1179 11.3073 19.1179L11.3173 19.1179C13.4573 19.1179 15.1973 17.3979 15.1973 15.2879L15.1973 10.5279C15.1973 10.5079 15.1773 10.4879 15.1573 10.4779L13.4173 9.48792L13.4173 15.2379C13.4173 15.4679 13.2873 15.6879 13.0773 15.8079L8.94734 18.1579Z M8.27654 17.0048L12.4465 14.6248C12.4665 14.6148 12.4765 14.5948 12.4765 14.5748L12.4765 14.5748L12.4765 12.5848L7.43654 15.4548C7.22654 15.5748 6.96654 15.5748 6.75654 15.4548L2.62654 13.1048C2.58654 13.0848 2.53654 13.0448 2.50654 13.0348C2.46654 13.2448 2.44654 13.4648 2.44654 13.6848C2.44654 14.3548 2.62654 15.0148 2.96654 15.6048L2.96654 15.5948C3.66654 16.7848 4.94654 17.5148 6.33654 17.5148C7.01654 17.5148 7.68654 17.3348 8.27654 17.0048Z M3.90324 5.16818C3.90324 5.12818 3.90324 5.06818 3.90324 5.02818C3.05324 5.33818 2.33324 5.92818 1.88324 6.70818L1.88324 6.70818C1.54324 7.28818 1.36324 7.94818 1.36324 8.61818C1.36324 9.98818 2.10324 11.2582 3.30324 11.9482L7.47324 14.3182C7.49324 14.3282 7.51324 14.3282 7.53324 14.3182L9.28324 13.3182L4.24324 10.4482C4.03324 10.3382 3.90324 10.1182 3.90324 9.87818L3.90324 9.87818L3.90324 5.16818Z M17.1561 8.50521L12.9761 6.1252C12.9561 6.1252 12.9361 6.1252 12.9161 6.1352L11.1761 7.1252L16.2161 9.9952C16.4261 10.1152 16.5561 10.3352 16.5561 10.5752C16.5561 10.5752 16.5561 10.5752 16.5561 10.5752L16.5561 15.4252C18.0761 14.8652 19.0961 13.4352 19.0961 11.8252C19.0961 10.4552 18.3561 9.1952 17.1561 8.50521Z M8.01418 5.82927C7.99418 5.83927 7.98418 5.85927 7.98418 5.87927L7.98418 5.87927L7.98418 7.86927L13.0242 4.99927C13.1242 4.93927 13.2442 4.90927 13.3642 4.90927C13.4842 4.90927 13.5942 4.93927 13.7042 4.99927L17.8342 7.34927C17.8742 7.36927 17.9142 7.39927 17.9542 7.41927L17.9542 7.41927C17.9842 7.20927 18.0042 6.98927 18.0042 6.76927C18.0042 4.65927 16.2642 2.93927 14.1242 2.93927C13.4442 2.93927 12.7742 3.11927 12.1842 3.44927L8.01418 5.82927Z M9.14676 1.33731C6.99676 1.33731 5.25676 3.05731 5.25676 5.16731L5.25676 9.92731C5.25676 9.94731 5.27676 9.95731 5.28676 9.96731L7.03676 10.9673L7.03676 5.22731L7.03676 5.21731C7.03676 4.98731 7.16676 4.76731 7.37676 4.64731L11.5068 2.29731C11.5468 2.26731 11.5968 2.23731 11.6268 2.22731C10.9268 1.64731 10.0468 1.33731 9.14676 1.33731Z M7.98345 11.5093L10.2235 12.7793L12.4735 11.5093L12.4735 8.9493L10.2235 7.6693L7.98345 8.9493L7.98345 11.5093Z " />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
|
||||
const cn = {
|
||||
WIP: "该功能仍在开发中……",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const de: LocaleType = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const en: LocaleType = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const es: LocaleType = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const it: LocaleType = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
|
||||
const jp = {
|
||||
WIP: "この機能は開発中です……",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const tr: LocaleType = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubmitKey } from "../store/app";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const tw: LocaleType = {
|
||||
@ -12,7 +12,7 @@ const tw: LocaleType = {
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
|
||||
Actions: {
|
||||
ChatList: "查看消息列表",
|
||||
ChatList: "查看訊息列表",
|
||||
CompressedHistory: "查看壓縮後的歷史 Prompt",
|
||||
Export: "匯出聊天紀錄",
|
||||
Copy: "複製",
|
||||
@ -32,10 +32,10 @@ const tw: LocaleType = {
|
||||
Send: "發送",
|
||||
},
|
||||
Export: {
|
||||
Title: "匯出聊天記錄為 Markdown",
|
||||
Title: "將聊天記錄匯出為 Markdown",
|
||||
Copy: "複製全部",
|
||||
Download: "下載檔案",
|
||||
MessageFromYou: "來自你的訊息",
|
||||
MessageFromYou: "來自您的訊息",
|
||||
MessageFromChatGPT: "來自 ChatGPT 的訊息",
|
||||
},
|
||||
Memory: {
|
||||
@ -43,8 +43,8 @@ const tw: LocaleType = {
|
||||
EmptyContent: "尚未記憶",
|
||||
Copy: "複製全部",
|
||||
Send: "發送記憶",
|
||||
Reset: "重置對話",
|
||||
ResetConfirm: "重置後將清空當前對話記錄以及歷史記憶,確認重置?",
|
||||
Reset: "重設對話",
|
||||
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "新的對話",
|
||||
@ -56,18 +56,18 @@ const tw: LocaleType = {
|
||||
Title: "設定",
|
||||
SubTitle: "設定選項",
|
||||
Actions: {
|
||||
ClearAll: "清除所有數據",
|
||||
ResetAll: "重置所有設定",
|
||||
ClearAll: "清除所有資料",
|
||||
ResetAll: "重設所有設定",
|
||||
Close: "關閉",
|
||||
ConfirmResetAll: {
|
||||
Confirm: "Are you sure you want to reset all configurations?",
|
||||
Confirm: "您確定要重設所有設定嗎?",
|
||||
},
|
||||
ConfirmClearAll: {
|
||||
Confirm: "Are you sure you want to reset all chat?",
|
||||
Confirm: "您確定要清除所有聊天嗎?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language",
|
||||
Name: "語言",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
@ -98,16 +98,16 @@ const tw: LocaleType = {
|
||||
SendPreviewBubble: "發送預覽氣泡",
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "停用提示詞自動補全",
|
||||
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補全",
|
||||
Title: "停用提示詞自動補齊",
|
||||
SubTitle: "在輸入框開頭輸入 / 即可觸發自動補齊",
|
||||
},
|
||||
List: "自定義提示詞列表",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`內置 ${builtin} 條,用戶定義 ${custom} 條`,
|
||||
`內建 ${builtin} 條,用戶定義 ${custom} 條`,
|
||||
Edit: "編輯",
|
||||
Modal: {
|
||||
Title: "提示詞列表",
|
||||
Add: "增加一條",
|
||||
Add: "新增一條",
|
||||
Search: "搜尋提示詞",
|
||||
},
|
||||
},
|
||||
@ -121,7 +121,7 @@ const tw: LocaleType = {
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自己的 Key 可規避授權訪問限制",
|
||||
SubTitle: "使用自己的 Key 可規避授權存取限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
EnableAOAI: "使用 Azure OpenAI",
|
||||
@ -133,7 +133,7 @@ const tw: LocaleType = {
|
||||
Usage: {
|
||||
Title: "帳戶餘額",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `本月已使用 $${used},订阅总额 $${total}`;
|
||||
return `本月已使用 $${used},訂閱總額 $${total}`;
|
||||
},
|
||||
IsChecking: "正在檢查…",
|
||||
Check: "重新檢查",
|
||||
@ -141,17 +141,17 @@ const tw: LocaleType = {
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "授權碼",
|
||||
SubTitle: "現在是未授權訪問狀態",
|
||||
SubTitle: "目前是未授權存取狀態",
|
||||
Placeholder: "請輸入授權碼",
|
||||
},
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "隨機性 (temperature)",
|
||||
SubTitle: "值越大,回復越隨機",
|
||||
SubTitle: "值越大,回應越隨機",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "單次回復限制 (max_tokens)",
|
||||
SubTitle: "單次交互所用的最大 Token 數",
|
||||
Title: "單次回應限制 (max_tokens)",
|
||||
SubTitle: "單次互動所用的最大 Token 數",
|
||||
},
|
||||
PresencePenlty: {
|
||||
Title: "話題新穎度 (presence_penalty)",
|
||||
@ -170,16 +170,16 @@ const tw: LocaleType = {
|
||||
Summarize:
|
||||
"Use the language used by the user (e.g. en-us for english conversation, zh-hant for chinese conversation, etc.) to summarise the conversation in at most 200 words. The summary will be used as prompt for you to continue the conversation in the future.",
|
||||
},
|
||||
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
||||
ConfirmClearAll: "確認清除所有對話、設定?",
|
||||
},
|
||||
Copy: {
|
||||
Success: "已複製到剪貼簿中",
|
||||
Failed: "複製失敗,請賦予剪貼簿權限",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `已設置 ${x} 條前置上下文`,
|
||||
Toast: (x: any) => `已設定 ${x} 條前置上下文`,
|
||||
Edit: "前置上下文和歷史記憶",
|
||||
Add: "新增壹條",
|
||||
Add: "新增一條",
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,12 @@
|
||||
import type { ChatRequest, ChatResponse } from "./api/openai/typing";
|
||||
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
|
||||
import {
|
||||
Message,
|
||||
ModelConfig,
|
||||
ModelType,
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
} from "./store";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
|
||||
const TIME_OUT_MS = 60000;
|
||||
@ -9,6 +16,7 @@ const makeRequestParam = (
|
||||
options?: {
|
||||
filterBot?: boolean;
|
||||
stream?: boolean;
|
||||
model?: ModelType;
|
||||
},
|
||||
): ChatRequest => {
|
||||
let sendMessages = messages.map((v) => ({
|
||||
@ -20,12 +28,17 @@ const makeRequestParam = (
|
||||
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
|
||||
}
|
||||
|
||||
const modelConfig = { ...useChatStore.getState().config.modelConfig };
|
||||
const modelConfig = { ...useAppConfig.getState().modelConfig };
|
||||
|
||||
// @yidadaa: wont send max_tokens, because it is nonsense for Muggles
|
||||
// @ts-expect-error
|
||||
delete modelConfig.max_tokens;
|
||||
|
||||
// override model config
|
||||
if (options?.model) {
|
||||
modelConfig.model = options.model;
|
||||
}
|
||||
|
||||
return {
|
||||
messages: sendMessages,
|
||||
stream: options?.stream,
|
||||
@ -60,7 +73,7 @@ function getRequestPath() {
|
||||
|
||||
export function requestOpenaiClient(path: string) {
|
||||
return (body: any, method = "POST") =>
|
||||
fetch("/api/openai?_vercel_no_cache=1", {
|
||||
fetch("/api/openai", {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -71,8 +84,16 @@ export function requestOpenaiClient(path: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestChat(messages: Message[]) {
|
||||
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
||||
export async function requestChat(
|
||||
messages: Message[],
|
||||
options?: {
|
||||
model?: ModelType;
|
||||
},
|
||||
) {
|
||||
const req: ChatRequest = makeRequestParam(messages, {
|
||||
filterBot: true,
|
||||
model: options?.model,
|
||||
});
|
||||
|
||||
const res = await requestOpenaiClient(getRequestPath())(req);
|
||||
|
||||
@ -139,6 +160,7 @@ export async function requestChatStream(
|
||||
options?: {
|
||||
filterBot?: boolean;
|
||||
modelConfig?: ModelConfig;
|
||||
model?: ModelType;
|
||||
onMessage: (message: string, done: boolean) => void;
|
||||
onError: (error: Error, statusCode?: number) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
@ -147,6 +169,7 @@ export async function requestChatStream(
|
||||
const req = makeRequestParam(messages, {
|
||||
stream: true,
|
||||
filterBot: options?.filterBot,
|
||||
model: options?.model,
|
||||
});
|
||||
|
||||
console.log("[Request] ", req);
|
||||
@ -214,7 +237,13 @@ export async function requestChatStream(
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestWithPrompt(messages: Message[], prompt: string) {
|
||||
export async function requestWithPrompt(
|
||||
messages: Message[],
|
||||
prompt: string,
|
||||
options?: {
|
||||
model?: ModelType;
|
||||
},
|
||||
) {
|
||||
messages = messages.concat([
|
||||
{
|
||||
role: "user",
|
||||
@ -223,7 +252,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await requestChat(messages);
|
||||
const res = await requestChat(messages, options);
|
||||
|
||||
return res?.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
173
app/store/app.ts
173
app/store/app.ts
@ -11,12 +11,14 @@ import { isMobileScreen, trimTopic } from "../utils";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import { ModelType, useAppConfig } from "./config";
|
||||
|
||||
export type Message = ChatCompletionResponseMessage & {
|
||||
date: string;
|
||||
streaming?: boolean;
|
||||
isError?: boolean;
|
||||
id?: number;
|
||||
model?: ModelType;
|
||||
};
|
||||
|
||||
export function createMessage(override: Partial<Message>): Message {
|
||||
@ -29,138 +31,8 @@ export function createMessage(override: Partial<Message>): Message {
|
||||
};
|
||||
}
|
||||
|
||||
export enum SubmitKey {
|
||||
Enter = "Enter",
|
||||
CtrlEnter = "Ctrl + Enter",
|
||||
ShiftEnter = "Shift + Enter",
|
||||
AltEnter = "Alt + Enter",
|
||||
MetaEnter = "Meta + Enter",
|
||||
}
|
||||
|
||||
export enum Theme {
|
||||
Auto = "auto",
|
||||
Dark = "dark",
|
||||
Light = "light",
|
||||
}
|
||||
|
||||
export interface ChatConfig {
|
||||
historyMessageCount: number; // -1 means all
|
||||
compressMessageLengthThreshold: number;
|
||||
sendBotMessages: boolean; // send bot's message or not
|
||||
submitKey: SubmitKey;
|
||||
avatar: string;
|
||||
fontSize: number;
|
||||
theme: Theme;
|
||||
tightBorder: boolean;
|
||||
sendPreviewBubble: boolean;
|
||||
sidebarWidth: number;
|
||||
|
||||
disablePromptHint: boolean;
|
||||
|
||||
modelConfig: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
presence_penalty: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type ModelConfig = ChatConfig["modelConfig"];
|
||||
|
||||
export const ROLES: Message["role"][] = ["system", "user", "assistant"];
|
||||
|
||||
const ENABLE_GPT4 = true;
|
||||
|
||||
export const AZURE_API_VERSION = [
|
||||
{
|
||||
name: "2023-03-15-preview",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const ALL_MODELS = [
|
||||
{
|
||||
name: "gpt-4",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0314",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0314",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
|
||||
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 limitModel(name: string) {
|
||||
return ALL_MODELS.some((m) => m.name === name && m.available)
|
||||
? name
|
||||
: ALL_MODELS[4].name;
|
||||
}
|
||||
|
||||
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,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
sendBotMessages: true as boolean,
|
||||
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
||||
avatar: "1f603",
|
||||
fontSize: 14,
|
||||
theme: Theme.Auto as Theme,
|
||||
tightBorder: false,
|
||||
sendPreviewBubble: true,
|
||||
sidebarWidth: 300,
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
modelConfig: {
|
||||
model: "gpt-3.5-turbo",
|
||||
temperature: 1,
|
||||
max_tokens: 2000,
|
||||
presence_penalty: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export interface ChatStat {
|
||||
tokenCount: number;
|
||||
wordCount: number;
|
||||
@ -206,7 +78,6 @@ function createEmptySession(): ChatSession {
|
||||
}
|
||||
|
||||
interface ChatStore {
|
||||
config: ChatConfig;
|
||||
sessions: ChatSession[];
|
||||
currentSessionIndex: number;
|
||||
clearSessions: () => void;
|
||||
@ -230,9 +101,6 @@ interface ChatStore {
|
||||
getMessagesWithMemory: () => Message[];
|
||||
getMemoryPrompt: () => Message;
|
||||
|
||||
getConfig: () => ChatConfig;
|
||||
resetConfig: () => void;
|
||||
updateConfig: (updater: (config: ChatConfig) => void) => void;
|
||||
clearAllData: () => void;
|
||||
}
|
||||
|
||||
@ -247,9 +115,6 @@ export const useChatStore = create<ChatStore>()(
|
||||
(set, get) => ({
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
config: {
|
||||
...DEFAULT_CONFIG,
|
||||
},
|
||||
|
||||
clearSessions() {
|
||||
set(() => ({
|
||||
@ -258,20 +123,6 @@ export const useChatStore = create<ChatStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
resetConfig() {
|
||||
set(() => ({ config: { ...DEFAULT_CONFIG } }));
|
||||
},
|
||||
|
||||
getConfig() {
|
||||
return get().config;
|
||||
},
|
||||
|
||||
updateConfig(updater) {
|
||||
const config = get().config;
|
||||
updater(config);
|
||||
set(() => ({ config }));
|
||||
},
|
||||
|
||||
selectSession(index: number) {
|
||||
set({
|
||||
currentSessionIndex: index,
|
||||
@ -394,6 +245,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
role: "assistant",
|
||||
streaming: true,
|
||||
id: userMessage.id! + 1,
|
||||
model: useAppConfig.getState().modelConfig.model,
|
||||
});
|
||||
|
||||
// get recent messages
|
||||
@ -446,8 +298,8 @@ export const useChatStore = create<ChatStore>()(
|
||||
controller,
|
||||
);
|
||||
},
|
||||
filterBot: !get().config.sendBotMessages,
|
||||
modelConfig: get().config.modelConfig,
|
||||
filterBot: !useAppConfig.getState().sendBotMessages,
|
||||
modelConfig: useAppConfig.getState().modelConfig,
|
||||
});
|
||||
},
|
||||
|
||||
@ -463,7 +315,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
|
||||
getMessagesWithMemory() {
|
||||
const session = get().currentSession();
|
||||
const config = get().config;
|
||||
const config = useAppConfig.getState();
|
||||
const messages = session.messages.filter((msg) => !msg.isError);
|
||||
const n = messages.length;
|
||||
|
||||
@ -538,24 +390,24 @@ export const useChatStore = create<ChatStore>()(
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
|
||||
) {
|
||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
|
||||
(res) => {
|
||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, {
|
||||
model: "gpt-3.5-turbo",
|
||||
}).then((res) => {
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const config = get().config;
|
||||
const config = useAppConfig.getState();
|
||||
let toBeSummarizedMsgs = session.messages.slice(
|
||||
session.lastSummarizeIndex,
|
||||
);
|
||||
|
||||
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||
|
||||
if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) {
|
||||
if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) {
|
||||
const n = toBeSummarizedMsgs.length;
|
||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||
Math.max(0, n - config.historyMessageCount),
|
||||
@ -586,6 +438,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
}),
|
||||
{
|
||||
filterBot: false,
|
||||
model: "gpt-3.5-turbo",
|
||||
onMessage(message, done) {
|
||||
session.memoryPrompt = message;
|
||||
if (done) {
|
||||
|
142
app/store/config.ts
Normal file
142
app/store/config.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export enum SubmitKey {
|
||||
Enter = "Enter",
|
||||
CtrlEnter = "Ctrl + Enter",
|
||||
ShiftEnter = "Shift + Enter",
|
||||
AltEnter = "Alt + Enter",
|
||||
MetaEnter = "Meta + Enter",
|
||||
}
|
||||
|
||||
export enum Theme {
|
||||
Auto = "auto",
|
||||
Dark = "dark",
|
||||
Light = "light",
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
historyMessageCount: 4,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
sendBotMessages: true as boolean,
|
||||
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
||||
avatar: "1f603",
|
||||
fontSize: 14,
|
||||
theme: Theme.Auto as Theme,
|
||||
tightBorder: false,
|
||||
sendPreviewBubble: true,
|
||||
sidebarWidth: 300,
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
modelConfig: {
|
||||
model: "gpt-3.5-turbo" as ModelType,
|
||||
temperature: 1,
|
||||
max_tokens: 2000,
|
||||
presence_penalty: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export type ChatConfig = typeof DEFAULT_CONFIG;
|
||||
|
||||
export type ChatConfigStore = ChatConfig & {
|
||||
reset: () => void;
|
||||
update: (updater: (config: ChatConfig) => void) => void;
|
||||
};
|
||||
|
||||
export type ModelConfig = ChatConfig["modelConfig"];
|
||||
|
||||
const ENABLE_GPT4 = true;
|
||||
|
||||
export const AZURE_API_VERSION = [
|
||||
{
|
||||
name: "2023-03-15-preview",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const ALL_MODELS = [
|
||||
{
|
||||
name: "gpt-4",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0314",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0314",
|
||||
available: ENABLE_GPT4,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ModelType = (typeof ALL_MODELS)[number]["name"];
|
||||
|
||||
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 limitModel(name: string) {
|
||||
return ALL_MODELS.some((m) => m.name === name && m.available)
|
||||
? name
|
||||
: ALL_MODELS[4].name;
|
||||
}
|
||||
|
||||
export const ModalConfigValidator = {
|
||||
model(x: string) {
|
||||
return limitModel(x) as ModelType;
|
||||
},
|
||||
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 CONFIG_KEY = "app-config";
|
||||
|
||||
export const useAppConfig = create<ChatConfigStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...DEFAULT_CONFIG,
|
||||
|
||||
reset() {
|
||||
set(() => ({ ...DEFAULT_CONFIG }));
|
||||
},
|
||||
|
||||
update(updater) {
|
||||
const config = { ...get() };
|
||||
updater(config);
|
||||
set(() => config);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: CONFIG_KEY,
|
||||
},
|
||||
),
|
||||
);
|
@ -1,3 +1,4 @@
|
||||
export * from "./app";
|
||||
export * from "./update";
|
||||
export * from "./access";
|
||||
export * from "./config";
|
||||
|
21
app/utils.ts
21
app/utils.ts
@ -1,4 +1,5 @@
|
||||
import { EmojiStyle } from "emoji-picker-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
|
||||
@ -47,7 +48,27 @@ export function isIOS() {
|
||||
return /iphone|ipad|ipod/.test(userAgent);
|
||||
}
|
||||
|
||||
export function useMobileScreen() {
|
||||
const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen());
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
setIsMobileScreen(isMobileScreen());
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isMobileScreen_;
|
||||
}
|
||||
|
||||
export function isMobileScreen() {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-breaks": "^3.0.2",
|
||||
|
@ -10,10 +10,20 @@ 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";
|
||||
|
||||
const timeoutPromise = (timeout) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Request timeout'));
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
async function fetchCN() {
|
||||
console.log("[Fetch] fetching cn prompts...");
|
||||
try {
|
||||
const raw = await (await fetch(CN_URL)).json();
|
||||
// const raw = await (await fetch(CN_URL)).json();
|
||||
const response = await Promise.race([fetch(CN_URL), timeoutPromise(5000)]);
|
||||
const raw = await response.json();
|
||||
return raw.map((v) => [v.act, v.prompt]);
|
||||
} catch (error) {
|
||||
console.error("[Fetch] failed to fetch cn prompts", error);
|
||||
@ -24,13 +34,15 @@ async function fetchCN() {
|
||||
async function fetchEN() {
|
||||
console.log("[Fetch] fetching en prompts...");
|
||||
try {
|
||||
const raw = await (await fetch(EN_URL)).text();
|
||||
// const raw = await (await fetch(EN_URL)).text();
|
||||
const response = await Promise.race([fetch(EN_URL), timeoutPromise(5000)]);
|
||||
const raw = await response.text();
|
||||
return raw
|
||||
.split("\n")
|
||||
.slice(1)
|
||||
.map((v) => v.split('","').map((v) => v.replace('"', "")));
|
||||
} catch (error) {
|
||||
console.error("[Fetch] failed to fetch cn prompts", error);
|
||||
console.error("[Fetch] failed to fetch en prompts", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -29,13 +29,13 @@ esac
|
||||
if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"ubuntu\"" ]]; then
|
||||
if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=ubuntu" ]]; then
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install nodejs git yarn
|
||||
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"centos\"" ]]; then
|
||||
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=centos" ]]; then
|
||||
sudo yum -y install epel-release
|
||||
sudo yum -y install nodejs git yarn
|
||||
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"arch\"" ]]; then
|
||||
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=arch" ]]; then
|
||||
sudo pacman -Syu -y
|
||||
sudo pacman -S -y nodejs git yarn
|
||||
else
|
||||
|
20
yarn.lock
20
yarn.lock
@ -1189,6 +1189,11 @@
|
||||
tiny-glob "^0.2.9"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@remix-run/router@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc"
|
||||
integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==
|
||||
|
||||
"@rushstack/eslint-patch@^1.1.3":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
|
||||
@ -4296,6 +4301,21 @@ react-redux@^8.0.4:
|
||||
react-is "^18.0.0"
|
||||
use-sync-external-store "^1.0.0"
|
||||
|
||||
react-router-dom@^6.10.0:
|
||||
version "6.10.0"
|
||||
resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.10.0.tgz#090ddc5c84dc41b583ce08468c4007c84245f61f"
|
||||
integrity sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.5.0"
|
||||
react-router "6.10.0"
|
||||
|
||||
react-router@6.10.0:
|
||||
version "6.10.0"
|
||||
resolved "https://registry.npmmirror.com/react-router/-/react-router-6.10.0.tgz#230f824fde9dd0270781b5cb497912de32c0a971"
|
||||
integrity sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.5.0"
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
|
Loading…
Reference in New Issue
Block a user