From 7e8973c9ffba853b46ea9d795b1a05e81828ed37 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 22:52:14 +0800 Subject: [PATCH 001/111] feat: close #291 gpt-4 model uses black icon --- app/components/chat.tsx | 12 +++++++++--- app/icons/black-bot.svg | 28 ++++++++++++++++++++++++++++ app/store/app.ts | 8 ++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 app/icons/black-bot.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 5dce8fd..d27c138 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -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,6 +31,7 @@ import { createMessage, useAccessStore, Theme, + ModelType, } from "../store"; import { @@ -64,13 +66,17 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { loading: () => , }); -export function Avatar(props: { role: Message["role"] }) { +export function Avatar(props: { role: Message["role"]; model?: ModelType }) { const config = useChatStore((state) => state.config); if (props.role !== "user") { return (
- + {props.model?.startsWith("gpt-4") ? ( + + ) : ( + + )}
); } @@ -727,7 +733,7 @@ export function Chat(props: { >
- +
{(message.preview || message.streaming) && (
diff --git a/app/icons/black-bot.svg b/app/icons/black-bot.svg new file mode 100644 index 0000000..3aad2ad --- /dev/null +++ b/app/icons/black-bot.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/store/app.ts b/app/store/app.ts index 8d875fe..5af4374 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -17,6 +17,7 @@ export type Message = ChatCompletionResponseMessage & { streaming?: boolean; isError?: boolean; id?: number; + model?: ModelType; }; export function createMessage(override: Partial): Message { @@ -58,7 +59,7 @@ export interface ChatConfig { disablePromptHint: boolean; modelConfig: { - model: string; + model: ModelType; temperature: number; max_tokens: number; presence_penalty: number; @@ -96,7 +97,9 @@ export const ALL_MODELS = [ name: "gpt-3.5-turbo-0301", available: true, }, -]; +] as const; + +export type ModelType = (typeof ALL_MODELS)[number]["name"]; export function limitNumber( x: number, @@ -387,6 +390,7 @@ export const useChatStore = create()( role: "assistant", streaming: true, id: userMessage.id! + 1, + model: get().config.modelConfig.model, }); // get recent messages From e3d2dd72794aa3d2b63c477231d54b0df62111e6 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 22:55:14 +0800 Subject: [PATCH 002/111] feat: close #427 add OPENAI_ORG_ID --- README.md | 4 ++++ README_CN.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 1764f2d..e9ec7e6 100644 --- a/README.md +++ b/README.md @@ -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#开发) diff --git a/README_CN.md b/README_CN.md index d2d64aa..03ec2a1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填 > 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。 +### `OPENAI_ORG_ID` (可选) + +指定 OpenAI 中的组织 ID。 + ## 开发 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 From 2e9e69d66c56a0d0a2468b6456477d156980126b Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 22:58:19 +0800 Subject: [PATCH 003/111] fixup --- app/store/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/app.ts b/app/store/app.ts index 5af4374..0a3ff86 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -122,7 +122,7 @@ export function limitModel(name: string) { export const ModalConfigValidator = { model(x: string) { - return limitModel(x); + return limitModel(x) as ModelType; }, max_tokens(x: number) { return limitNumber(x, 0, 32000, 2000); From 06d503152bcba1ad9175441709d7e5c3044eea0a Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 23:04:58 +0800 Subject: [PATCH 004/111] feat: close #928 summarize with gpt-3.5 --- app/requests.ts | 38 ++++++++++++++++++++++++++++++++------ app/store/app.ts | 16 ++++++++-------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/app/requests.ts b/app/requests.ts index 3cb838e..9159f1c 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -1,5 +1,11 @@ import type { ChatRequest, ChatResponse } from "./api/openai/typing"; -import { Message, ModelConfig, useAccessStore, useChatStore } from "./store"; +import { + Message, + ModelConfig, + ModelType, + useAccessStore, + useChatStore, +} from "./store"; import { showToast } from "./components/ui-lib"; const TIME_OUT_MS = 60000; @@ -9,6 +15,7 @@ const makeRequestParam = ( options?: { filterBot?: boolean; stream?: boolean; + model?: ModelType; }, ): ChatRequest => { let sendMessages = messages.map((v) => ({ @@ -26,6 +33,11 @@ const makeRequestParam = ( // @ts-expect-error delete modelConfig.max_tokens; + // override model config + if (options?.model) { + modelConfig.model = options.model; + } + return { messages: sendMessages, stream: options?.stream, @@ -50,7 +62,7 @@ function getHeaders() { 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", @@ -61,8 +73,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("v1/chat/completions")(req); @@ -204,7 +224,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", @@ -213,7 +239,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 ?? ""; } diff --git a/app/store/app.ts b/app/store/app.ts index 0a3ff86..fe2a07d 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -535,14 +535,14 @@ export const useChatStore = create()( session.topic === DEFAULT_TOPIC && countMessages(session.messages) >= SUMMARIZE_MIN_LEN ) { - requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then( - (res) => { - get().updateCurrentSession( - (session) => - (session.topic = res ? trimTopic(res) : DEFAULT_TOPIC), - ); - }, - ); + 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; From 2390da11651c80bd3e0fd3935063614a5694aa02 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Thu, 20 Apr 2023 23:09:42 +0800 Subject: [PATCH 005/111] fix: #930 wont show delete for first message --- app/components/chat.tsx | 66 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index d27c138..02f4614 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -723,6 +723,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 (
- {(message.preview || message.streaming) && ( + {showTyping && (
{Locale.Chat.Typing}
)}
- {!isUser && - !(message.preview || message.content.length === 0) && ( -
- {message.streaming ? ( -
onUserStop(message.id ?? i)} - > - {Locale.Chat.Actions.Stop} -
- ) : ( - <> -
onDelete(message.id ?? i)} - > - {Locale.Chat.Actions.Delete} -
-
onResend(message.id ?? i)} - > - {Locale.Chat.Actions.Retry} -
- - )} - + {showActions && ( +
+ {message.streaming ? (
copyToClipboard(message.content)} + onClick={() => onUserStop(message.id ?? i)} > - {Locale.Chat.Actions.Copy} + {Locale.Chat.Actions.Stop}
+ ) : ( + <> +
onDelete(message.id ?? i)} + > + {Locale.Chat.Actions.Delete} +
+
onResend(message.id ?? i)} + > + {Locale.Chat.Actions.Retry} +
+ + )} + +
copyToClipboard(message.content)} + > + {Locale.Chat.Actions.Copy}
- )} +
+ )} Date: Thu, 20 Apr 2023 23:20:25 +0800 Subject: [PATCH 006/111] feat: reactive isMobileScreen --- app/components/chat-list.tsx | 1 - app/components/chat.tsx | 13 +++++++------ app/components/home.tsx | 20 +++++++++----------- app/utils.ts | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index cab8812..f013920 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -10,7 +10,6 @@ import { import { useChatStore } from "../store"; import Locale from "../locales"; -import { isMobileScreen } from "../utils"; export function ChatItem(props: { onClick?: () => void; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 02f4614..c6bc61e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -38,9 +38,9 @@ import { copyToClipboard, downloadAs, getEmojiUrl, - isMobileScreen, selectOrCopy, autoGrowTextArea, + useMobileScreen, } from "../utils"; import dynamic from "next/dynamic"; @@ -438,6 +438,7 @@ export function Chat(props: { const { submitKey, shouldSubmit } = useSubmitHandler(); const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(false); + const isMobileScreen = useMobileScreen(); const onChatBodyScroll = (e: HTMLElement) => { const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; @@ -468,7 +469,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); }, @@ -508,7 +509,7 @@ export function Chat(props: { setBeforeInput(userInput); setUserInput(""); setPromptHints([]); - if (!isMobileScreen()) inputRef.current?.focus(); + if (!isMobileScreen) inputRef.current?.focus(); setAutoScroll(true); }; @@ -640,7 +641,7 @@ export function Chat(props: { // Auto focus useEffect(() => { - if (props.sideBarShowing && isMobileScreen()) return; + if (props.sideBarShowing && isMobileScreen) return; inputRef.current?.focus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -688,7 +689,7 @@ export function Chat(props: { }} />
- {!isMobileScreen() && ( + {!isMobileScreen && (
: } @@ -788,7 +789,7 @@ export function Chat(props: { } onContextMenu={(e) => onRightClick(e, message)} onDoubleClickCapture={() => { - if (!isMobileScreen()) return; + if (!isMobileScreen) return; setUserInput(message.content); }} fontSize={fontSize} diff --git a/app/components/home.tsx b/app/components/home.tsx index 828b757..ef30243 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -17,7 +17,7 @@ import LoadingIcon from "../icons/three-dots.svg"; import CloseIcon from "../icons/close.svg"; import { useChatStore } from "../store"; -import { getCSSVar, isMobileScreen } from "../utils"; +import { getCSSVar, useMobileScreen } from "../utils"; import Locale from "../locales"; import { Chat } from "./chat"; @@ -103,17 +103,14 @@ function useDragSideBar() { window.addEventListener("mousemove", handleMouseMove.current); window.addEventListener("mouseup", handleMouseUp.current); }; + const isMobileScreen = useMobileScreen(); useEffect(() => { - if (isMobileScreen()) { - return; - } - - document.documentElement.style.setProperty( - "--sidebar-width", - `${limit(chatStore.config.sidebarWidth ?? 300)}px`, - ); - }, [chatStore.config.sidebarWidth]); + const sideBarWidth = isMobileScreen + ? "100vw" + : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; + document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); + }, [chatStore.config.sidebarWidth, isMobileScreen]); return { onDragMouseDown, @@ -148,6 +145,7 @@ function _Home() { // drag side bar const { onDragMouseDown } = useDragSideBar(); + const isMobileScreen = useMobileScreen(); useSwitchTheme(); @@ -158,7 +156,7 @@ function _Home() { return (
{ + const onResize = () => { + setIsMobileScreen(isMobileScreen()); + }; + + window.addEventListener("resize", onResize); + + return () => { + window.removeEventListener("resize", onResize); + }; + }, []); + + return isMobileScreen_; +} + export function isMobileScreen() { return window.innerWidth <= 600; } From 693dcf12d6c6ddd610b12bbc85ebab0474e46256 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 01:12:39 +0800 Subject: [PATCH 007/111] refactor: close #643 use react router --- app/components/chat-list.tsx | 8 +- app/components/chat.tsx | 14 +-- app/components/home.tsx | 212 +++++++++-------------------------- app/components/settings.tsx | 10 +- app/components/sidebar.tsx | 135 ++++++++++++++++++++++ app/constant.ts | 6 + app/utils.ts | 2 +- package.json | 1 + yarn.lock | 20 ++++ 9 files changed, 234 insertions(+), 174 deletions(-) create mode 100644 app/components/sidebar.tsx diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index f013920..fb0f740 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -10,6 +10,8 @@ import { import { useChatStore } from "../store"; import Locale from "../locales"; +import { Link, useNavigate } from "react-router-dom"; +import { Path } from "../constant"; export function ChatItem(props: { onClick?: () => void; @@ -59,6 +61,7 @@ export function ChatList() { state.moveSession, ]); const chatStore = useChatStore(); + const navigate = useNavigate(); const onDragEnd: OnDragEndResponder = (result) => { const { destination, source } = result; @@ -94,7 +97,10 @@ export function ChatList() { id={item.id} index={i} selected={i === selectedIndex} - onClick={() => selectSession(i)} + onClick={() => { + navigate(Path.Chat); + selectSession(i); + }} onDelete={() => chatStore.deleteSession(i)} /> ))} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c6bc61e..bab4229 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -54,6 +54,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), @@ -418,10 +420,7 @@ export function ChatActions(props: { ); } -export function Chat(props: { - showSideBar?: () => void; - sideBarShowing?: boolean; -}) { +export function Chat() { type RenderMessage = Message & { preview?: boolean }; const chatStore = useChatStore(); @@ -439,6 +438,7 @@ export function Chat(props: { 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; @@ -641,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 }, []); @@ -666,7 +666,7 @@ export function Chat(props: { icon={} bordered title={Locale.Chat.Actions.ChatList} - onClick={props?.showSideBar} + onClick={() => navigate(Path.Home)} />
@@ -830,7 +830,7 @@ export function Chat(props: { setAutoScroll(false); setTimeout(() => setPromptHints([]), 500); }} - autoFocus={!props?.sideBarShowing} + autoFocus rows={inputRows} /> +
{!props.noLogo && }
@@ -38,7 +38,7 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); -const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { +const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, { loading: () => , }); @@ -73,50 +73,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); - }; - const isMobileScreen = useMobileScreen(); - - useEffect(() => { - const sideBarWidth = isMobileScreen - ? "100vw" - : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; - document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); - }, [chatStore.config.sidebarWidth, isMobileScreen]); - - return { - onDragMouseDown, - }; -} - const useHasHydrated = () => { const [hasHydrated, setHasHydrated] = useState(false); @@ -127,130 +83,64 @@ 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); - +function WideScreen() { // setting - const [openSettings, setOpenSettings] = useState(false); const config = useChatStore((state) => state.config); - // drag side bar - const { onDragMouseDown } = useDragSideBar(); - const isMobileScreen = useMobileScreen(); - - useSwitchTheme(); - - if (loading) { - return ; - } - return (
-
-
-
ChatGPT Next
-
- Build your own AI assistant. -
-
- -
-
- -
{ - setOpenSettings(false); - setShowSideBar(false); - }} - > - -
- -
-
-
- } - onClick={chatStore.deleteSession} - /> -
-
- } - onClick={() => { - setOpenSettings(true); - setShowSideBar(false); - }} - shadow - /> -
- -
-
- } - text={Locale.Home.NewChat} - onClick={() => { - createNewSession(); - setShowSideBar(false); - }} - shadow - /> -
-
- -
onDragMouseDown(e as any)} - >
+
+
- {openSettings ? ( - { - setOpenSettings(false); - setShowSideBar(true); - }} - /> - ) : ( - setShowSideBar(true)} - sideBarShowing={showSideBar} - /> - )} + + } /> + } /> + } /> + +
+
+ ); +} + +function MobileScreen() { + const location = useLocation(); + const isHome = location.pathname === Path.Home; + + return ( +
+
+ +
+ +
+ + + } /> + } /> +
); } export function Home() { + useSwitchTheme(); + + const isMobileScreen = useMobileScreen(); + + if (!useHasHydrated()) { + return ; + } + return ( - <_Home> + {isMobileScreen ? : } ); } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index d81b5b3..0214849 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -29,10 +29,11 @@ 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,7 +177,8 @@ function PasswordInput(props: HTMLProps) { ); } -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) => [ @@ -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 }) {
} - onClick={props.closeSettings} + onClick={() => navigate(Path.Home)} bordered title={Locale.Settings.Actions.Close} /> diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx new file mode 100644 index 0000000..338dec1 --- /dev/null +++ b/app/components/sidebar.tsx @@ -0,0 +1,135 @@ +import { useState, 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 { useChatStore } from "../store"; + +import { Path, REPO_URL } from "../constant"; + +import { HashRouter as Router, Link, useNavigate } from "react-router-dom"; +import { useMobileScreen } from "../utils"; +import { ChatList } from "./chat-list"; + +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); + }; + const isMobileScreen = useMobileScreen(); + + useEffect(() => { + const sideBarWidth = isMobileScreen + ? "100vw" + : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; + document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); + }, [chatStore.config.sidebarWidth, isMobileScreen]); + + return { + onDragMouseDown, + }; +} + +export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { + const chatStore = useChatStore(); + + // drag side bar + const { onDragMouseDown } = useDragSideBar(); + const navigate = useNavigate(); + const isMobileScreen = useMobileScreen(); + + return ( + <> +
+
ChatGPT Next
+
+ Build your own AI assistant. +
+
+ +
+
+ +
{ + if (e.target === e.currentTarget) { + navigate(Path.Home); + } + props.setShowSideBar?.(false); + }} + > + +
+ +
+
+
+ } + onClick={chatStore.deleteSession} + /> +
+
+ + } shadow /> + +
+ +
+
+ } + text={Locale.Home.NewChat} + onClick={() => { + chatStore.newSession(); + props.setShowSideBar?.(false); + }} + shadow + /> +
+
+ +
onDragMouseDown(e as any)} + >
+ + ); +} diff --git a/app/constant.ts b/app/constant.ts index 6f08ad7..6874454 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -6,3 +6,9 @@ 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", +} diff --git a/app/utils.ts b/app/utils.ts index 9d6e906..0af9fef 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -49,7 +49,7 @@ export function isIOS() { } export function useMobileScreen() { - const [isMobileScreen_, setIsMobileScreen] = useState(false); + const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen()); useEffect(() => { const onResize = () => { setIsMobileScreen(isMobileScreen()); diff --git a/package.json b/package.json index 19047ad..7850055 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 342ea4a..b7d9f83 100644 --- a/yarn.lock +++ b/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" From 5185166e3b5b1f2b802833c79565c51c766b912e Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 01:18:49 +0800 Subject: [PATCH 008/111] fixup --- app/components/home.tsx | 2 -- app/utils.ts | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index efca5c0..8b5e1d7 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -21,7 +21,6 @@ import { HashRouter as Router, Routes, Route, - useNavigation, useLocation, } from "react-router-dom"; @@ -131,7 +130,6 @@ function MobileScreen() { export function Home() { useSwitchTheme(); - const isMobileScreen = useMobileScreen(); if (!useHasHydrated()) { diff --git a/app/utils.ts b/app/utils.ts index 0af9fef..dfec8d3 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -66,6 +66,9 @@ export function useMobileScreen() { } export function isMobileScreen() { + if (typeof window === "undefined") { + return false; + } return window.innerWidth <= 600; } From 82ad0573be93b0ee43f9cc52b865ea8878988dfa Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 02:52:53 +0800 Subject: [PATCH 009/111] feat: close #380 collapse side bar --- app/components/chat-list.tsx | 31 ++++++++++----- app/components/home.module.scss | 69 ++++++++++++++++++++++++++++++++- app/components/home.tsx | 13 ++----- app/components/sidebar.tsx | 43 ++++++++++++-------- app/constant.ts | 4 ++ 5 files changed, 125 insertions(+), 35 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index fb0f740..626336a 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -22,6 +22,7 @@ export function ChatItem(props: { selected: boolean; id: number; index: number; + narrow?: boolean; }) { return ( @@ -35,13 +36,20 @@ export function ChatItem(props: { {...provided.draggableProps} {...provided.dragHandleProps} > -
{props.title}
-
-
- {Locale.ChatItem.ChatItemCount(props.count)} -
-
{props.time}
-
+ {props.narrow ? ( +
{props.count}
+ ) : ( + <> +
{props.title}
+
+
+ {Locale.ChatItem.ChatItemCount(props.count)} +
+
{props.time}
+
+ + )} +
@@ -51,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, @@ -101,7 +109,12 @@ export function ChatList() { navigate(Path.Chat); selectSession(i); }} - onDelete={() => chatStore.deleteSession(i)} + onDelete={() => { + if (!props.narrow || confirm(Locale.Home.DeleteChat)) { + chatStore.deleteSession(i); + } + }} + narrow={props.narrow} /> ))} {provided.placeholder} diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 9bf0d57..38e755b 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -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; diff --git a/app/components/home.tsx b/app/components/home.tsx index 8b5e1d7..123be03 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -41,7 +41,7 @@ const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, { loading: () => , }); -function useSwitchTheme() { +export function useSwitchTheme() { const config = useChatStore((state) => state.config); useEffect(() => { @@ -83,7 +83,6 @@ const useHasHydrated = () => { }; function WideScreen() { - // setting const config = useChatStore((state) => state.config); return ( @@ -92,9 +91,7 @@ function WideScreen() { config.tightBorder ? styles["tight-container"] : styles.container }`} > -
- -
+
@@ -113,9 +110,7 @@ function MobileScreen() { return (
-
- -
+
@@ -129,8 +124,8 @@ function MobileScreen() { } export function Home() { - useSwitchTheme(); const isMobileScreen = useMobileScreen(); + useSwitchTheme(); if (!useHasHydrated()) { return ; diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 338dec1..71e75f8 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -12,14 +12,20 @@ import Locale from "../locales"; import { useChatStore } from "../store"; -import { Path, REPO_URL } from "../constant"; +import { + MAX_SIDEBAR_WIDTH, + MIN_SIDEBAR_WIDTH, + NARROW_SIDEBAR_WIDTH, + Path, + REPO_URL, +} from "../constant"; import { HashRouter as Router, Link, useNavigate } from "react-router-dom"; import { useMobileScreen } from "../utils"; import { ChatList } from "./chat-list"; function useDragSideBar() { - const limit = (x: number) => Math.min(500, Math.max(220, x)); + const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); const chatStore = useChatStore(); const startX = useRef(0); @@ -27,7 +33,7 @@ function useDragSideBar() { const lastUpdateTime = useRef(Date.now()); const handleMouseMove = useRef((e: MouseEvent) => { - if (Date.now() < lastUpdateTime.current + 100) { + if (Date.now() < lastUpdateTime.current + 50) { return; } lastUpdateTime.current = Date.now(); @@ -49,29 +55,36 @@ function useDragSideBar() { window.addEventListener("mouseup", handleMouseUp.current); }; const isMobileScreen = useMobileScreen(); + const shouldNarrow = + !isMobileScreen && chatStore.config.sidebarWidth < MIN_SIDEBAR_WIDTH; useEffect(() => { - const sideBarWidth = isMobileScreen - ? "100vw" - : `${limit(chatStore.config.sidebarWidth ?? 300)}px`; + const barWidth = shouldNarrow + ? NARROW_SIDEBAR_WIDTH + : limit(chatStore.config.sidebarWidth ?? 300); + const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`; document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); - }, [chatStore.config.sidebarWidth, isMobileScreen]); + }, [chatStore.config.sidebarWidth, isMobileScreen, shouldNarrow]); return { onDragMouseDown, + shouldNarrow, }; } -export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { +export function SideBar(props: { className?: string }) { const chatStore = useChatStore(); // drag side bar - const { onDragMouseDown } = useDragSideBar(); + const { onDragMouseDown, shouldNarrow } = useDragSideBar(); const navigate = useNavigate(); - const isMobileScreen = useMobileScreen(); return ( - <> +
ChatGPT Next
@@ -88,10 +101,9 @@ export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { if (e.target === e.currentTarget) { navigate(Path.Home); } - props.setShowSideBar?.(false); }} > - +
@@ -116,10 +128,9 @@ export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) {
} - text={Locale.Home.NewChat} + text={shouldNarrow ? undefined : Locale.Home.NewChat} onClick={() => { chatStore.newSession(); - props.setShowSideBar?.(false); }} shadow /> @@ -130,6 +141,6 @@ export function SideBar(props: { setShowSideBar?: (_: boolean) => void }) { className={styles["sidebar-drag"]} onMouseDown={(e) => onDragMouseDown(e as any)} >
- +
); } diff --git a/app/constant.ts b/app/constant.ts index 6874454..43ae4cc 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -12,3 +12,7 @@ export enum Path { Chat = "/chat", Settings = "/settings", } + +export const MAX_SIDEBAR_WIDTH = 500; +export const MIN_SIDEBAR_WIDTH = 230; +export const NARROW_SIDEBAR_WIDTH = 100; From b6a7104b60b462f79cbdd7be6b5b8f4285196b62 Mon Sep 17 00:00:00 2001 From: Shi Liang <7258605+shih-liang@users.noreply.github.com> Date: Fri, 21 Apr 2023 13:03:02 +0800 Subject: [PATCH 010/111] chat-stream: runtime = "experimental-edge"; --- app/api/chat-stream/route.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/chat-stream/route.ts b/app/api/chat-stream/route.ts index 41f1354..22550e3 100644 --- a/app/api/chat-stream/route.ts +++ b/app/api/chat-stream/route.ts @@ -59,6 +59,4 @@ export async function POST(req: NextRequest) { } } -export const config = { - runtime: "edge", -}; +export const runtime = "experimental-edge"; From 8966fd3b23935e86212840e17577d18d263ccac9 Mon Sep 17 00:00:00 2001 From: Shi Liang <7258605+shih-liang@users.noreply.github.com> Date: Fri, 21 Apr 2023 13:03:38 +0800 Subject: [PATCH 011/111] openai runtime = "experimental-edge"; --- app/api/openai/route.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts index 0ac94bd..bed70d9 100644 --- a/app/api/openai/route.ts +++ b/app/api/openai/route.ts @@ -30,6 +30,4 @@ export async function GET(req: NextRequest) { return makeRequest(req); } -export const config = { - runtime: "edge", -}; +export const runtime = "experimental-edge"; From 596a46846ad675c6a9304bd59700a13a47b5653e Mon Sep 17 00:00:00 2001 From: jzjwonderful Date: Fri, 21 Apr 2023 17:26:40 +0800 Subject: [PATCH 012/111] fix bug 978 --- scripts/setup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index b965333..751a9ac 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -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 From 4d45c07bf2096e9f12c142c010e3893c905d35f1 Mon Sep 17 00:00:00 2001 From: Zhenyu Zhu Date: Fri, 21 Apr 2023 18:52:32 +0800 Subject: [PATCH 013/111] fix: adjust presence_penalty step 0.1 --- app/components/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 0214849..4b552e2 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -645,7 +645,7 @@ export function Settings() { value={config.modelConfig.presence_penalty?.toFixed(1)} min="-2" max="2" - step="0.5" + step="0.1" onChange={(e) => { updateConfig( (config) => From 209a727fe92d9dac8e33c49a83efef702c661a7e Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 23:22:02 +0800 Subject: [PATCH 014/111] feat: close #928 summarize with gpt3.5 --- app/requests.ts | 2 ++ app/store/app.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/app/requests.ts b/app/requests.ts index 9159f1c..ce72bb7 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -149,6 +149,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; @@ -157,6 +158,7 @@ export async function requestChatStream( const req = makeRequestParam(messages, { stream: true, filterBot: options?.filterBot, + model: options?.model, }); console.log("[Request] ", req); diff --git a/app/store/app.ts b/app/store/app.ts index fe2a07d..89995e0 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -583,6 +583,7 @@ export const useChatStore = create()( }), { filterBot: false, + model: "gpt-3.5-turbo", onMessage(message, done) { session.memoryPrompt = message; if (done) { From e1ce1f2f4002abbb0cb86cf688957457e92afb90 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 23:28:53 +0800 Subject: [PATCH 015/111] feat: close #976 esc to close modal --- app/components/ui-lib.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index a72aa86..ffc05cf 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -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 (
From ab826363ea4d585becb70d53778d45c0aa312403 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 21 Apr 2023 23:37:25 +0800 Subject: [PATCH 016/111] fix: #965 improve loading animation --- app/components/home.module.scss | 5 ++++- app/components/home.tsx | 7 ++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 38e755b..1c021d8 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -313,6 +313,10 @@ .chat-message { display: flex; flex-direction: row; + + &:last-child { + animation: slide-in ease 0.3s; + } } .chat-message-user { @@ -325,7 +329,6 @@ display: flex; flex-direction: column; align-items: flex-start; - animation: slide-in ease 0.3s; &:hover { .chat-message-top-actions { diff --git a/app/components/home.tsx b/app/components/home.tsx index 123be03..ac3ce90 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -2,7 +2,7 @@ require("../polyfill"); -import { useState, useEffect } from "react"; +import { useState, useEffect, StyleHTMLAttributes } from "react"; import styles from "./home.module.scss"; @@ -23,6 +23,7 @@ import { Route, useLocation, } from "react-router-dom"; +import { SideBar } from "./sidebar"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -37,10 +38,6 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); -const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, { - loading: () => , -}); - export function useSwitchTheme() { const config = useChatStore((state) => state.config); From ae479f4a92d1f5a20cfd5265a932bc329a029d58 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 22 Apr 2023 00:12:07 +0800 Subject: [PATCH 017/111] fix: #963 config not work --- app/components/chat.tsx | 22 ++--- app/components/home.tsx | 6 +- app/components/settings.tsx | 16 ++-- app/components/sidebar.tsx | 26 +++--- app/locales/cn.ts | 2 +- app/locales/de.ts | 2 +- app/locales/en.ts | 2 +- app/locales/es.ts | 2 +- app/locales/it.ts | 2 +- app/locales/jp.ts | 2 +- app/locales/tr.ts | 2 +- app/locales/tw.ts | 2 +- app/requests.ts | 3 +- app/store/app.ts | 159 ++---------------------------------- app/store/config.ts | 135 ++++++++++++++++++++++++++++++ app/store/index.ts | 1 + 16 files changed, 190 insertions(+), 194 deletions(-) create mode 100644 app/store/config.ts diff --git a/app/components/chat.tsx b/app/components/chat.tsx index bab4229..c5cc542 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -32,6 +32,7 @@ import { useAccessStore, Theme, ModelType, + useAppConfig, } from "../store"; import { @@ -69,7 +70,7 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { }); export function Avatar(props: { role: Message["role"]; model?: ModelType }) { - const config = useChatStore((state) => state.config); + const config = useAppConfig(); if (props.role !== "user") { return ( @@ -285,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) => { @@ -361,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 @@ -428,7 +429,8 @@ export function Chat() { state.currentSession(), state.currentSessionIndex, ]); - const fontSize = useChatStore((state) => state.config.fontSize); + const config = useAppConfig(); + const fontSize = config.fontSize; const inputRef = useRef(null); const [userInput, setUserInput] = useState(""); @@ -492,7 +494,7 @@ export function Chat() { // 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); @@ -583,8 +585,6 @@ export function Chat() { inputRef.current?.focus(); }; - const config = useChatStore((state) => state.config); - const context: RenderMessage[] = session.context.slice(); const accessStore = useAccessStore(); @@ -692,10 +692,10 @@ export function Chat() { {!isMobileScreen && (
: } + icon={config.tightBorder ? : } bordered onClick={() => { - chatStore.updateConfig( + config.update( (config) => (config.tightBorder = !config.tightBorder), ); }} diff --git a/app/components/home.tsx b/app/components/home.tsx index ac3ce90..3233402 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -9,7 +9,6 @@ import styles from "./home.module.scss"; import BotIcon from "../icons/bot.svg"; import LoadingIcon from "../icons/three-dots.svg"; -import { useChatStore } from "../store"; import { getCSSVar, useMobileScreen } from "../utils"; import { Chat } from "./chat"; @@ -24,6 +23,7 @@ import { useLocation, } from "react-router-dom"; import { SideBar } from "./sidebar"; +import { useAppConfig } from "../store/config"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -39,7 +39,7 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, { }); export function useSwitchTheme() { - const config = useChatStore((state) => state.config); + const config = useAppConfig(); useEffect(() => { document.body.classList.remove("light"); @@ -80,7 +80,7 @@ const useHasHydrated = () => { }; function WideScreen() { - const config = useChatStore((state) => state.config); + const config = useAppConfig(); return (
) { 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, - state.clearAllData, - state.clearSessions, - ]); + const config = useAppConfig(); + const updateConfig = config.update; + const resetConfig = config.reset; + const [clearAllData, clearSessions] = useChatStore((state) => [ + state.clearAllData, + state.clearSessions, + ]); const updateStore = useUpdateStore(); const [checkingUpdate, setCheckingUpdate] = useState(false); diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 71e75f8..d0c99dd 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import styles from "./home.module.scss"; @@ -10,7 +10,7 @@ import AddIcon from "../icons/add.svg"; import CloseIcon from "../icons/close.svg"; import Locale from "../locales"; -import { useChatStore } from "../store"; +import { useAppConfig, useChatStore } from "../store"; import { MAX_SIDEBAR_WIDTH, @@ -20,16 +20,20 @@ import { REPO_URL, } from "../constant"; -import { HashRouter as Router, Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useMobileScreen } from "../utils"; -import { ChatList } from "./chat-list"; +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 chatStore = useChatStore(); + const config = useAppConfig(); const startX = useRef(0); - const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300); + const startDragWidth = useRef(config.sidebarWidth ?? 300); const lastUpdateTime = useRef(Date.now()); const handleMouseMove = useRef((e: MouseEvent) => { @@ -39,11 +43,11 @@ function useDragSideBar() { lastUpdateTime.current = Date.now(); const d = e.clientX - startX.current; const nextWidth = limit(startDragWidth.current + d); - chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth)); + config.update((config) => (config.sidebarWidth = nextWidth)); }); const handleMouseUp = useRef(() => { - startDragWidth.current = chatStore.config.sidebarWidth ?? 300; + startDragWidth.current = config.sidebarWidth ?? 300; window.removeEventListener("mousemove", handleMouseMove.current); window.removeEventListener("mouseup", handleMouseUp.current); }); @@ -56,15 +60,15 @@ function useDragSideBar() { }; const isMobileScreen = useMobileScreen(); const shouldNarrow = - !isMobileScreen && chatStore.config.sidebarWidth < MIN_SIDEBAR_WIDTH; + !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH; useEffect(() => { const barWidth = shouldNarrow ? NARROW_SIDEBAR_WIDTH - : limit(chatStore.config.sidebarWidth ?? 300); + : limit(config.sidebarWidth ?? 300); const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`; document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); - }, [chatStore.config.sidebarWidth, isMobileScreen, shouldNarrow]); + }, [config.sidebarWidth, isMobileScreen, shouldNarrow]); return { onDragMouseDown, diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 1c19819..777cea5 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; const cn = { WIP: "该功能仍在开发中……", diff --git a/app/locales/de.ts b/app/locales/de.ts index e71abfa..42a4c8f 100644 --- a/app/locales/de.ts +++ b/app/locales/de.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const de: LocaleType = { diff --git a/app/locales/en.ts b/app/locales/en.ts index 20e5696..f7af4bf 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const en: LocaleType = { diff --git a/app/locales/es.ts b/app/locales/es.ts index e2a9eb2..efecf11 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const es: LocaleType = { diff --git a/app/locales/it.ts b/app/locales/it.ts index f0453b5..b519ef4 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const it: LocaleType = { diff --git a/app/locales/jp.ts b/app/locales/jp.ts index 2818820..1c8d66d 100644 --- a/app/locales/jp.ts +++ b/app/locales/jp.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; const jp = { WIP: "この機能は開発中です……", diff --git a/app/locales/tr.ts b/app/locales/tr.ts index 04a8462..86f1f41 100644 --- a/app/locales/tr.ts +++ b/app/locales/tr.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const tr: LocaleType = { diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 44c0789..20e41f4 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -1,4 +1,4 @@ -import { SubmitKey } from "../store/app"; +import { SubmitKey } from "../store/config"; import type { LocaleType } from "./index"; const tw: LocaleType = { diff --git a/app/requests.ts b/app/requests.ts index ce72bb7..0e75709 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -4,6 +4,7 @@ import { ModelConfig, ModelType, useAccessStore, + useAppConfig, useChatStore, } from "./store"; import { showToast } from "./components/ui-lib"; @@ -27,7 +28,7 @@ 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 diff --git a/app/store/app.ts b/app/store/app.ts index 89995e0..2294130 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -11,6 +11,7 @@ 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; @@ -30,133 +31,8 @@ export function createMessage(override: Partial): 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: ModelType; - 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 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 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; @@ -202,7 +78,6 @@ function createEmptySession(): ChatSession { } interface ChatStore { - config: ChatConfig; sessions: ChatSession[]; currentSessionIndex: number; clearSessions: () => void; @@ -226,9 +101,6 @@ interface ChatStore { getMessagesWithMemory: () => Message[]; getMemoryPrompt: () => Message; - getConfig: () => ChatConfig; - resetConfig: () => void; - updateConfig: (updater: (config: ChatConfig) => void) => void; clearAllData: () => void; } @@ -243,9 +115,6 @@ export const useChatStore = create()( (set, get) => ({ sessions: [createEmptySession()], currentSessionIndex: 0, - config: { - ...DEFAULT_CONFIG, - }, clearSessions() { set(() => ({ @@ -254,20 +123,6 @@ export const useChatStore = create()( })); }, - 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, @@ -390,7 +245,7 @@ export const useChatStore = create()( role: "assistant", streaming: true, id: userMessage.id! + 1, - model: get().config.modelConfig.model, + model: useAppConfig.getState().modelConfig.model, }); // get recent messages @@ -443,8 +298,8 @@ export const useChatStore = create()( controller, ); }, - filterBot: !get().config.sendBotMessages, - modelConfig: get().config.modelConfig, + filterBot: !useAppConfig.getState().sendBotMessages, + modelConfig: useAppConfig.getState().modelConfig, }); }, @@ -460,7 +315,7 @@ export const useChatStore = create()( 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; @@ -545,14 +400,14 @@ export const useChatStore = create()( }); } - 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), diff --git a/app/store/config.ts b/app/store/config.ts new file mode 100644 index 0000000..346f38d --- /dev/null +++ b/app/store/config.ts @@ -0,0 +1,135 @@ +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 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()( + persist( + (set, get) => ({ + ...DEFAULT_CONFIG, + + reset() { + set(() => ({ ...DEFAULT_CONFIG })); + }, + + update(updater) { + const config = { ...get() }; + updater(config); + set(() => config); + }, + }), + { + name: CONFIG_KEY, + }, + ), +); diff --git a/app/store/index.ts b/app/store/index.ts index 3bdb58c..7b7bbd0 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -1,3 +1,4 @@ export * from "./app"; export * from "./update"; export * from "./access"; +export * from "./config"; From a3ca8ea5c458a8453c21095b65c88305125243ab Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 22 Apr 2023 00:35:50 +0800 Subject: [PATCH 018/111] feat: new chat-item avatar --- app/components/chat-list.tsx | 14 +++++++++++++- app/components/home.module.scss | 24 ++++++++++++++++++++---- app/components/sidebar.tsx | 2 +- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 626336a..637e0b1 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -1,4 +1,6 @@ import DeleteIcon from "../icons/delete.svg"; +import BotIcon from "../icons/bot.svg"; + import styles from "./home.module.scss"; import { DragDropContext, @@ -35,9 +37,19 @@ export function ChatItem(props: { ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} + title={`${props.title}\n${Locale.ChatItem.ChatItemCount( + props.count, + )}`} > {props.narrow ? ( -
{props.count}
+
+
+ +
+
+ {props.count} +
+
) : ( <>
{props.title}
diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 1c021d8..b0b44d9 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -154,7 +154,6 @@ user-select: none; border: 2px solid transparent; position: relative; - overflow: hidden; } .chat-item:hover { @@ -228,6 +227,7 @@ justify-content: center; align-items: center; transition: all ease 0.3s; + overflow: hidden; &:hover { .chat-item-narrow { @@ -237,15 +237,31 @@ } .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; + display: flex; + flex-direction: column; + justify-content: center; + + .chat-item-avatar { + display: flex; + justify-content: center; + opacity: 0.1; + position: absolute; + transform: scale(4); + } + + .chat-item-narrow-count { + font-size: 24px; + font-weight: bolder; + text-align: center; + color: var(--primary); + opacity: 0.6; + } } .chat-item-delete { diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index d0c99dd..1e35964 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -94,7 +94,7 @@ export function SideBar(props: { className?: string }) {
Build your own AI assistant.
-
+
From 79f58f5c6ad61e321c24c039e8e17607bd8d0397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B2=E9=9C=A7?= <123147018+yunwuu@users.noreply.github.com> Date: Sat, 22 Apr 2023 00:47:15 +0800 Subject: [PATCH 019/111] fix: typo --- app/components/chat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c5cc542..b80bf5a 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -545,7 +545,7 @@ export function Chat() { } }; - 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) { @@ -568,14 +568,14 @@ export function Chat() { }; 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); From 4cdb2f0fa37c9e97dd4dafe490955a57a5940370 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 22 Apr 2023 01:13:23 +0800 Subject: [PATCH 020/111] feat: session-level model config --- app/components/home.module.scss | 4 +- app/components/home.tsx | 7 +- app/components/settings.module.scss | 14 -- app/components/settings.tsx | 271 ++++++++++++++-------------- app/components/ui-lib.tsx | 29 ++- app/store/app.ts | 11 +- app/store/config.ts | 4 +- app/styles/globals.scss | 14 ++ 8 files changed, 187 insertions(+), 167 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index b0b44d9..7476c08 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -138,9 +138,7 @@ .sidebar-body { flex: 1; overflow: auto; -} - -.chat-list { + overflow-x: hidden; } .chat-item { diff --git a/app/components/home.tsx b/app/components/home.tsx index 3233402..851dba1 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -2,7 +2,7 @@ require("../polyfill"); -import { useState, useEffect, StyleHTMLAttributes } from "react"; +import { useState, useEffect } from "react"; import styles from "./home.module.scss"; @@ -10,7 +10,6 @@ import BotIcon from "../icons/bot.svg"; import LoadingIcon from "../icons/three-dots.svg"; import { getCSSVar, useMobileScreen } from "../utils"; -import { Chat } from "./chat"; import dynamic from "next/dynamic"; import { Path } from "../constant"; @@ -38,6 +37,10 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); +const Chat = dynamic(async () => (await import("./chat")).Chat, { + loading: () => , +}); + export function useSwitchTheme() { const config = useAppConfig(); diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index b7f0955..9df76d3 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -19,20 +19,6 @@ 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; diff --git a/app/components/settings.tsx b/app/components/settings.tsx index ae41287..1b2b4c7 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -9,10 +9,7 @@ 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 { Input, List, ListItem, Modal, Popover } from "./ui-lib"; +import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib"; import { IconButton } from "./button"; import { @@ -24,6 +21,8 @@ import { useAccessStore, ModalConfigValidator, useAppConfig, + ChatConfig, + ModelConfig, } from "../store"; import { Avatar } from "./chat"; @@ -155,26 +154,127 @@ function SettingItem(props: { ); } -function PasswordInput(props: HTMLProps) { - const [visible, setVisible] = useState(false); - - function changeVisibility() { - setVisible(!visible); - } - +export function ModelConfigList(props: { + modelConfig: ModelConfig; + updateConfig: (updater: (config: ModelConfig) => void) => void; +}) { return ( -
- : } - onClick={changeVisibility} - className={styles["password-eye"]} - /> - -
+ <> + + + + + { + props.updateConfig( + (config) => + (config.temperature = ModalConfigValidator.temperature( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + + + + props.updateConfig( + (config) => + (config.max_tokens = ModalConfigValidator.max_tokens( + e.currentTarget.valueAsNumber, + )), + ) + } + > + + + { + props.updateConfig( + (config) => + (config.presence_penalty = + ModalConfigValidator.presence_penalty( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + + + + + props.updateConfig( + (config) => (config.historyMessageCount = e.target.valueAsNumber), + ) + } + > + + + + + props.updateConfig( + (config) => + (config.compressMessageLengthThreshold = + e.currentTarget.valueAsNumber), + ) + } + > + + ); } @@ -505,44 +605,6 @@ export function Settings() { /> )} - - - - updateConfig( - (config) => - (config.historyMessageCount = e.target.valueAsNumber), - ) - } - > - - - - - updateConfig( - (config) => - (config.compressMessageLengthThreshold = - e.currentTarget.valueAsNumber), - ) - } - > - @@ -578,85 +640,14 @@ export function Settings() { - - - - - { - updateConfig( - (config) => - (config.modelConfig.temperature = - ModalConfigValidator.temperature( - e.currentTarget.valueAsNumber, - )), - ); - }} - > - - - - updateConfig( - (config) => - (config.modelConfig.max_tokens = - ModalConfigValidator.max_tokens( - e.currentTarget.valueAsNumber, - )), - ) - } - > - - - { - updateConfig( - (config) => - (config.modelConfig.presence_penalty = - ModalConfigValidator.presence_penalty( - e.currentTarget.valueAsNumber, - )), - ); - }} - > - + { + const modelConfig = { ...config.modelConfig }; + upater(modelConfig); + config.update((config) => (config.modelConfig = modelConfig)); + }} + /> {shouldShowPromptModal && ( diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index ffc05cf..8e04db3 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -1,8 +1,12 @@ import styles from "./ui-lib.module.scss"; import LoadingIcon from "../icons/three-dots.svg"; import CloseIcon from "../icons/close.svg"; +import EyeIcon from "../icons/eye.svg"; +import EyeOffIcon from "../icons/eye-off.svg"; + import { createRoot } from "react-dom/client"; -import React, { useEffect } from "react"; +import React, { HTMLProps, useEffect, useState } from "react"; +import { IconButton } from "./button"; export function Popover(props: { children: JSX.Element; @@ -190,3 +194,26 @@ export function Input(props: InputProps) { > ); } + +export function PasswordInput(props: HTMLProps) { + const [visible, setVisible] = useState(false); + + function changeVisibility() { + setVisible(!visible); + } + + return ( +
+ : } + onClick={changeVisibility} + className={"password-eye"} + /> + +
+ ); +} diff --git a/app/store/app.ts b/app/store/app.ts index 2294130..652e26f 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -334,14 +334,14 @@ export const useChatStore = create()( // get short term and unmemoried long term memory const shortTermMemoryMessageIndex = Math.max( 0, - n - config.historyMessageCount, + n - config.modelConfig.historyMessageCount, ); const longTermMemoryMessageIndex = session.lastSummarizeIndex; const oldestIndex = Math.max( shortTermMemoryMessageIndex, longTermMemoryMessageIndex, ); - const threshold = config.compressMessageLengthThreshold; + const threshold = config.modelConfig.compressMessageLengthThreshold; // get recent messages as many as possible const reversedRecentMessages = []; @@ -410,7 +410,7 @@ export const useChatStore = create()( if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) { const n = toBeSummarizedMsgs.length; toBeSummarizedMsgs = toBeSummarizedMsgs.slice( - Math.max(0, n - config.historyMessageCount), + Math.max(0, n - config.modelConfig.historyMessageCount), ); } @@ -423,11 +423,12 @@ export const useChatStore = create()( "[Chat History] ", toBeSummarizedMsgs, historyMsgLength, - config.compressMessageLengthThreshold, + config.modelConfig.compressMessageLengthThreshold, ); if ( - historyMsgLength > config.compressMessageLengthThreshold && + historyMsgLength > + config.modelConfig.compressMessageLengthThreshold && session.sendMemory ) { requestChatStream( diff --git a/app/store/config.ts b/app/store/config.ts index 346f38d..9373340 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -16,8 +16,6 @@ export enum Theme { } const DEFAULT_CONFIG = { - historyMessageCount: 4, - compressMessageLengthThreshold: 1000, sendBotMessages: true as boolean, submitKey: SubmitKey.CtrlEnter as SubmitKey, avatar: "1f603", @@ -34,6 +32,8 @@ const DEFAULT_CONFIG = { temperature: 1, max_tokens: 2000, presence_penalty: 0, + historyMessageCount: 4, + compressMessageLengthThreshold: 1000, }, }; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 37c6622..5815d74 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -311,3 +311,17 @@ pre { overflow: auto; } } + +.password-input-container { + max-width: 50%; + display: flex; + justify-content: flex-end; + + .password-eye { + margin-right: 4px; + } + + .password-input { + min-width: 80%; + } +} From 5d2fb8791ccaacc6baf873a26c842fd1c47e9427 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 23 Apr 2023 00:07:16 +0900 Subject: [PATCH 021/111] Update README.md Github -> GitHub --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e9ec7e6..a6d1e3d 100644 --- a/README.md +++ b/README.md @@ -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: ![Automatic Updates](./docs/images/enable-actions.jpg) @@ -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. From 1761289716aba1e6c6745d7e313dd837e463b4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B2=E9=9C=A7?= <123147018+yunwuu@users.noreply.github.com> Date: Sat, 22 Apr 2023 23:53:58 +0800 Subject: [PATCH 022/111] fix: typo --- scripts/fetch-prompts.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-prompts.mjs b/scripts/fetch-prompts.mjs index 7f6818d..9dc7262 100644 --- a/scripts/fetch-prompts.mjs +++ b/scripts/fetch-prompts.mjs @@ -30,7 +30,7 @@ async function fetchEN() { .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 []; } } From 818629e58bdc96b30e83320ed863a41d4118bf96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B2=E9=9C=A7?= <123147018+yunwuu@users.noreply.github.com> Date: Sun, 23 Apr 2023 00:17:00 +0800 Subject: [PATCH 023/111] chore: add timeout to prompt download request --- scripts/fetch-prompts.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/fetch-prompts.mjs b/scripts/fetch-prompts.mjs index 9dc7262..689377c 100644 --- a/scripts/fetch-prompts.mjs +++ b/scripts/fetch-prompts.mjs @@ -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,7 +34,9 @@ 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 = response.text(); return raw .split("\n") .slice(1) From 7345639af33aede885afe6828a0969cf1f9a4a2d Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 23 Apr 2023 01:27:15 +0800 Subject: [PATCH 024/111] feat: add session config modal --- app/components/chat.module.scss | 36 ++-- app/components/chat.tsx | 323 +++++++++++++++------------- app/components/emoji.tsx | 59 +++++ app/components/home.module.scss | 11 - app/components/model-config.tsx | 141 ++++++++++++ app/components/settings.module.scss | 10 - app/components/settings.tsx | 235 ++++---------------- app/components/ui-lib.module.scss | 12 ++ app/components/ui-lib.tsx | 24 ++- app/locales/cn.ts | 6 +- app/store/{app.ts => chat.ts} | 11 +- app/store/config.ts | 1 + app/store/index.ts | 2 +- app/styles/globals.scss | 11 + app/utils.ts | 5 - 15 files changed, 489 insertions(+), 398 deletions(-) create mode 100644 app/components/emoji.tsx create mode 100644 app/components/model-config.tsx rename app/store/{app.ts => chat.ts} (98%) diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 7cd2889..3a1be39 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -53,6 +53,20 @@ } } +.section-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; + + .section-title-action { + display: flex; + align-items: center; + } +} + .context-prompt { .context-prompt-row { display: flex; @@ -81,25 +95,13 @@ } .memory-prompt { - margin-top: 20px; - - .memory-prompt-title { - 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; - } - } + margin: 20px 0; .memory-prompt-content { - background-color: var(--gray); - border-radius: 6px; + background-color: var(--white); + color: var(--black); + border: var(--border-in-light); + border-radius: 10px; padding: 10px; font-size: 12px; user-select: text; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c5cc542..867fbc4 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,4 +1,4 @@ -import { useDebounce, useDebouncedCallback } from "use-debounce"; +import { useDebouncedCallback } from "use-debounce"; import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; @@ -9,8 +9,6 @@ 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 BlackBotIcon from "../icons/black-bot.svg"; import AddIcon from "../icons/add.svg"; import DeleteIcon from "../icons/delete.svg"; import MaxIcon from "../icons/max.svg"; @@ -33,12 +31,13 @@ import { Theme, ModelType, useAppConfig, + ModelConfig, + DEFAULT_TOPIC, } from "../store"; import { copyToClipboard, downloadAs, - getEmojiUrl, selectOrCopy, autoGrowTextArea, useMobileScreen, @@ -54,10 +53,11 @@ import { IconButton } from "./button"; import styles from "./home.module.scss"; import chatStyle from "./chat.module.scss"; -import { Input, Modal, showModal } from "./ui-lib"; +import { Input, List, ListItem, Modal, Popover, showModal } from "./ui-lib"; import { useNavigate } from "react-router-dom"; import { Path } from "../constant"; - +import { ModelConfigList } from "./model-config"; +import { AvatarPicker } from "./emoji"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), { @@ -65,32 +65,10 @@ const Markdown = dynamic( }, ); -const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { +const Avatar = dynamic(async () => (await import("./emoji")).Avatar, { loading: () => , }); -export function Avatar(props: { role: Message["role"]; model?: ModelType }) { - const config = useAppConfig(); - - if (props.role !== "user") { - return ( -
- {props.model?.startsWith("gpt-4") ? ( - - ) : ( - - )} -
- ); - } - - return ( -
- -
- ); -} - function exportMessages(messages: Message[], topic: string) { const mdText = `# ${topic}\n\n` + @@ -129,15 +107,13 @@ function exportMessages(messages: Message[], topic: string) { }); } -function PromptToast(props: { - showToast?: boolean; - showModal?: boolean; - setShowModal: (_: boolean) => void; -}) { +function ContextPrompts() { const chatStore = useChatStore(); const session = chatStore.currentSession(); const context = session.context; + const [showPicker, setShowPicker] = useState(false); + const addContextPrompt = (prompt: Message) => { chatStore.updateCurrentSession((session) => { session.context.push(prompt); @@ -156,6 +132,165 @@ function PromptToast(props: { }); }; + return ( + <> +
+ {context.map((c, i) => ( +
+ + + updateContextPrompt(i, { + ...c, + content: e.currentTarget.value as any, + }) + } + /> + } + className={chatStyle["context-delete-button"]} + onClick={() => removeContextPrompt(i)} + bordered + /> +
+ ))} + +
+ } + text={Locale.Context.Add} + bordered + className={chatStyle["context-prompt-button"]} + onClick={() => + addContextPrompt({ + role: "system", + content: "", + date: "", + }) + } + /> +
+
+ + + + chatStore.updateCurrentSession( + (session) => (session.avatar = emoji), + ) + } + > + } + open={showPicker} + onClose={() => setShowPicker(false)} + > +
setShowPicker(true)}> + {session.avatar ? ( + + ) : ( + + )} +
+
+
+ + + chatStore.updateCurrentSession( + (session) => (session.topic = e.currentTarget.value), + ) + } + > + + +
+ + ); +} + +export function SessionConfigModel(props: { onClose: () => void }) { + const chatStore = useChatStore(); + const config = useAppConfig(); + const session = chatStore.currentSession(); + const context = session.context; + + const updateConfig = (updater: (config: ModelConfig) => void) => { + const config = { ...session.modelConfig }; + updater(config); + chatStore.updateCurrentSession((session) => (session.modelConfig = config)); + }; + + return ( +
+ props.onClose()} + actions={[ + } + bordered + text="重置预设" + onClick={() => + confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession() + } + />, + } + bordered + text="保存预设" + onClick={() => copyToClipboard(session.memoryPrompt)} + />, + ]} + > + + + + +
+ ); +} + +function PromptToast(props: { + showToast?: boolean; + showModal?: boolean; + setShowModal: (_: boolean) => void; +}) { + const chatStore = useChatStore(); + const session = chatStore.currentSession(); + const context = session.context; + return (
{props.showToast && ( @@ -171,115 +306,7 @@ function PromptToast(props: {
)} {props.showModal && ( -
- props.setShowModal(false)} - actions={[ - } - bordered - text={Locale.Memory.Reset} - onClick={() => - confirm(Locale.Memory.ResetConfirm) && - chatStore.resetSession() - } - />, - } - bordered - text={Locale.Memory.Copy} - onClick={() => copyToClipboard(session.memoryPrompt)} - />, - ]} - > - <> -
- {context.map((c, i) => ( -
- - - updateContextPrompt(i, { - ...c, - content: e.currentTarget.value as any, - }) - } - /> - } - className={chatStyle["context-delete-button"]} - onClick={() => removeContextPrompt(i)} - bordered - /> -
- ))} - -
- } - text={Locale.Context.Add} - bordered - className={chatStyle["context-prompt-button"]} - onClick={() => - addContextPrompt({ - role: "system", - content: "", - date: "", - }) - } - /> -
-
-
-
- - {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "} - {session.messages.length}) - - - -
-
- {session.memoryPrompt || Locale.Memory.EmptyContent} -
-
- -
-
+ props.setShowModal(false)} /> )}
); @@ -654,7 +681,7 @@ export function Chat() { className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`} onClickCapture={renameSession} > - {session.topic} + {!session.topic ? DEFAULT_TOPIC : session.topic}
{Locale.Chat.SubTitle(session.messages.length)} @@ -739,7 +766,13 @@ export function Chat() { >
- + {message.role === "user" ? ( + + ) : session.avatar ? ( + + ) : ( + + )}
{showTyping && (
diff --git a/app/components/emoji.tsx b/app/components/emoji.tsx new file mode 100644 index 0000000..b1d092a --- /dev/null +++ b/app/components/emoji.tsx @@ -0,0 +1,59 @@ +import EmojiPicker, { + Emoji, + EmojiStyle, + Theme as EmojiTheme, +} from "emoji-picker-react"; + +import { ModelType } from "../store"; + +import BotIcon from "../icons/bot.svg"; +import BlackBotIcon from "../icons/black-bot.svg"; + +export function getEmojiUrl(unified: string, style: EmojiStyle) { + return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; +} + +export function AvatarPicker(props: { + onEmojiClick: (emojiId: string) => void; +}) { + return ( + { + props.onEmojiClick(e.unified); + }} + /> + ); +} + +export function Avatar(props: { model?: ModelType; avatar?: string }) { + if (props.model) { + return ( +
+ {props.model?.startsWith("gpt-4") ? ( + + ) : ( + + )} +
+ ); + } + + return ( +
+ {props.avatar && } +
+ ); +} + +export function EmojiAvatar(props: { avatar: string; size?: number }) { + return ( + + ); +} diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 7476c08..be630e1 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -368,17 +368,6 @@ margin-top: 5px; } -.user-avtar { - height: 30px; - width: 30px; - display: flex; - align-items: center; - justify-content: center; - border: var(--border-in-light); - box-shadow: var(--card-shadow); - border-radius: 10px; -} - .chat-message-item { box-sizing: border-box; max-width: 100%; diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx new file mode 100644 index 0000000..2b6d59f --- /dev/null +++ b/app/components/model-config.tsx @@ -0,0 +1,141 @@ +import styles from "./settings.module.scss"; +import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store"; + +import Locale from "../locales"; +import { InputRange } from "./input-range"; +import { List, ListItem } from "./ui-lib"; + +export function ModelConfigList(props: { + modelConfig: ModelConfig; + updateConfig: (updater: (config: ModelConfig) => void) => void; +}) { + return ( + + + + + + { + props.updateConfig( + (config) => + (config.temperature = ModalConfigValidator.temperature( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + + + + props.updateConfig( + (config) => + (config.max_tokens = ModalConfigValidator.max_tokens( + e.currentTarget.valueAsNumber, + )), + ) + } + > + + + { + props.updateConfig( + (config) => + (config.presence_penalty = + ModalConfigValidator.presence_penalty( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + + + + + props.updateConfig( + (config) => (config.historyMessageCount = e.target.valueAsNumber), + ) + } + > + + + + + props.updateConfig( + (config) => + (config.compressMessageLengthThreshold = + e.currentTarget.valueAsNumber), + ) + } + > + + + + props.updateConfig( + (config) => (config.sendMemory = e.currentTarget.checked), + ) + } + > + + + ); +} diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 9df76d3..6fb5a68 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -5,16 +5,6 @@ overflow: auto; } -.settings-title { - font-size: 14px; - font-weight: bolder; -} - -.settings-sub-title { - font-size: 12px; - font-weight: normal; -} - .avatar { cursor: pointer; } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 1b2b4c7..ffe540a 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1,7 +1,5 @@ import { useState, useEffect, useMemo, HTMLProps, useRef } from "react"; -import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react"; - import styles from "./settings.module.scss"; import ResetIcon from "../icons/reload.svg"; @@ -10,30 +8,27 @@ import CopyIcon from "../icons/copy.svg"; import ClearIcon from "../icons/clear.svg"; import EditIcon from "../icons/edit.svg"; import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib"; +import { ModelConfigList } from "./model-config"; import { IconButton } from "./button"; import { SubmitKey, useChatStore, Theme, - ALL_MODELS, useUpdateStore, useAccessStore, - ModalConfigValidator, useAppConfig, - ChatConfig, - ModelConfig, } from "../store"; -import { Avatar } from "./chat"; import Locale, { AllLangs, changeLang, getLang } from "../locales"; -import { copyToClipboard, getEmojiUrl } from "../utils"; +import { copyToClipboard } from "../utils"; import Link from "next/link"; 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"; +import { Avatar, AvatarPicker } from "./emoji"; function UserPromptModal(props: { onClose?: () => void }) { const promptStore = usePromptStore(); @@ -136,148 +131,6 @@ function UserPromptModal(props: { onClose?: () => void }) { ); } -function SettingItem(props: { - title: string; - subTitle?: string; - children: JSX.Element; -}) { - return ( - -
-
{props.title}
- {props.subTitle && ( -
{props.subTitle}
- )} -
- {props.children} -
- ); -} - -export function ModelConfigList(props: { - modelConfig: ModelConfig; - updateConfig: (updater: (config: ModelConfig) => void) => void; -}) { - return ( - <> - - - - - { - props.updateConfig( - (config) => - (config.temperature = ModalConfigValidator.temperature( - e.currentTarget.valueAsNumber, - )), - ); - }} - > - - - - props.updateConfig( - (config) => - (config.max_tokens = ModalConfigValidator.max_tokens( - e.currentTarget.valueAsNumber, - )), - ) - } - > - - - { - props.updateConfig( - (config) => - (config.presence_penalty = - ModalConfigValidator.presence_penalty( - e.currentTarget.valueAsNumber, - )), - ); - }} - > - - - - - props.updateConfig( - (config) => (config.historyMessageCount = e.target.valueAsNumber), - ) - } - > - - - - - props.updateConfig( - (config) => - (config.compressMessageLengthThreshold = - e.currentTarget.valueAsNumber), - ) - } - > - - - ); -} - export function Settings() { const navigate = useNavigate(); const [showEmojiPicker, setShowEmojiPicker] = useState(false); @@ -401,16 +254,13 @@ export function Settings() {
- + setShowEmojiPicker(false)} content={ - { - updateConfig((config) => (config.avatar = e.unified)); + { + updateConfig((config) => (config.avatar = avatar)); setShowEmojiPicker(false); }} /> @@ -421,12 +271,12 @@ export function Settings() { className={styles.avatar} onClick={() => setShowEmojiPicker(true)} > - +
- + - checkUpdate(true)} /> )} - + - + - + - -
- {Locale.Settings.Theme} -
+ - + - +
- @@ -521,9 +368,9 @@ export function Settings() { ) } > - + - + - + - + - + {enabledAccessControl ? ( - @@ -563,12 +410,12 @@ export function Settings() { accessStore.updateCode(e.currentTarget.value); }} /> - + ) : ( <> )} - @@ -580,9 +427,9 @@ export function Settings() { accessStore.updateToken(e.currentTarget.value); }} /> - + - )} - + - @@ -622,9 +469,9 @@ export function Settings() { ) } > - + - setShowPromptModal(true)} /> - + - - { - const modelConfig = { ...config.modelConfig }; - upater(modelConfig); - config.update((config) => (config.modelConfig = modelConfig)); - }} - /> - + { + const modelConfig = { ...config.modelConfig }; + upater(modelConfig); + config.update((config) => (config.modelConfig = modelConfig)); + }} + /> {shouldShowPromptModal && ( setShowPromptModal(false)} /> diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index 8965c06..e3acd6d 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -35,6 +35,16 @@ border-bottom: var(--border-in-light); padding: 10px 20px; animation: slide-in ease 0.6s; + + .list-item-title { + font-size: 14px; + font-weight: bolder; + } + + .list-item-sub-title { + font-size: 12px; + font-weight: normal; + } } .list { @@ -89,6 +99,8 @@ padding: var(--modal-padding); display: flex; justify-content: flex-end; + border-top: var(--border-in-light); + box-shadow: var(--shadow); .modal-actions { display: flex; diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 8e04db3..4a92461 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -33,12 +33,22 @@ export function Card(props: { children: JSX.Element[]; className?: string }) { ); } -export function ListItem(props: { children: JSX.Element[] }) { - if (props.children.length > 2) { - throw Error("Only Support Two Children"); - } - - return
{props.children}
; +export function ListItem(props: { + title: string; + subTitle?: string; + children?: JSX.Element | JSX.Element[]; +}) { + return ( +
+
+
{props.title}
+ {props.subTitle && ( +
{props.subTitle}
+ )} +
+ {props.children} +
+ ); } export function List(props: { children: JSX.Element[] | JSX.Element }) { @@ -63,7 +73,7 @@ export function Loading() { interface ModalProps { title: string; - children?: JSX.Element; + children?: JSX.Element | JSX.Element[]; actions?: JSX.Element[]; onClose?: () => void; } diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 777cea5..2e35cb3 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -39,7 +39,7 @@ const cn = { }, Memory: { Title: "历史摘要", - EmptyContent: "尚未总结", + EmptyContent: "对话内容过短,无需总结", Send: "启用总结并发送摘要", Copy: "复制摘要", Reset: "重置对话", @@ -172,8 +172,8 @@ const cn = { }, Context: { Toast: (x: any) => `已设置 ${x} 条前置上下文`, - Edit: "前置上下文和历史记忆", - Add: "新增一条", + Edit: "当前对话设置", + Add: "新增预设对话", }, }; diff --git a/app/store/app.ts b/app/store/chat.ts similarity index 98% rename from app/store/app.ts rename to app/store/chat.ts index 652e26f..fcea406 100644 --- a/app/store/app.ts +++ b/app/store/chat.ts @@ -11,7 +11,7 @@ import { isMobileScreen, trimTopic } from "../utils"; import Locale from "../locales"; import { showToast } from "../components/ui-lib"; -import { ModelType, useAppConfig } from "./config"; +import { ModelConfig, ModelType, useAppConfig } from "./config"; export type Message = ChatCompletionResponseMessage & { date: string; @@ -42,16 +42,18 @@ export interface ChatStat { export interface ChatSession { id: number; topic: string; - sendMemory: boolean; + avatar?: string; memoryPrompt: string; context: Message[]; messages: Message[]; stat: ChatStat; lastUpdate: string; lastSummarizeIndex: number; + + modelConfig: ModelConfig; } -const DEFAULT_TOPIC = Locale.Store.DefaultTopic; +export const DEFAULT_TOPIC = Locale.Store.DefaultTopic; export const BOT_HELLO: Message = createMessage({ role: "assistant", content: Locale.Store.BotHello, @@ -63,7 +65,6 @@ function createEmptySession(): ChatSession { return { id: Date.now(), topic: DEFAULT_TOPIC, - sendMemory: true, memoryPrompt: "", context: [], messages: [], @@ -74,6 +75,8 @@ function createEmptySession(): ChatSession { }, lastUpdate: createDate, lastSummarizeIndex: 0, + + modelConfig: useAppConfig.getState().modelConfig, }; } diff --git a/app/store/config.ts b/app/store/config.ts index 9373340..1e60460 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -32,6 +32,7 @@ const DEFAULT_CONFIG = { temperature: 1, max_tokens: 2000, presence_penalty: 0, + sendMemory: true, historyMessageCount: 4, compressMessageLengthThreshold: 1000, }, diff --git a/app/store/index.ts b/app/store/index.ts index 7b7bbd0..0760f48 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -1,4 +1,4 @@ -export * from "./app"; +export * from "./chat"; export * from "./update"; export * from "./access"; export * from "./config"; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 5815d74..c5ffacb 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -325,3 +325,14 @@ pre { min-width: 80%; } } + +.user-avtar { + height: 30px; + width: 30px; + display: flex; + align-items: center; + justify-content: center; + border: var(--border-in-light); + box-shadow: var(--card-shadow); + border-radius: 10px; +} diff --git a/app/utils.ts b/app/utils.ts index dfec8d3..0ebcc93 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,4 +1,3 @@ -import { EmojiStyle } from "emoji-picker-react"; import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; @@ -90,10 +89,6 @@ export function selectOrCopy(el: HTMLElement, content: string) { return true; } -export function getEmojiUrl(unified: string, style: EmojiStyle) { - return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; -} - function getDomContentWidth(dom: HTMLElement) { const style = window.getComputedStyle(dom); const paddingWidth = From b23adf9d5dd3b835d245bd471523d438993557db Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 23 Apr 2023 01:37:47 +0800 Subject: [PATCH 025/111] fixup --- app/api/chat-stream/route.ts | 2 +- app/api/config/route.ts | 2 + app/api/openai/route.ts | 4 +- app/components/chat.tsx | 112 ++++++++++++++++---------------- app/components/model-config.tsx | 4 +- app/components/settings.tsx | 18 ++--- app/store/chat.ts | 14 ++-- app/store/config.ts | 2 +- 8 files changed, 83 insertions(+), 75 deletions(-) diff --git a/app/api/chat-stream/route.ts b/app/api/chat-stream/route.ts index 22550e3..2775ff0 100644 --- a/app/api/chat-stream/route.ts +++ b/app/api/chat-stream/route.ts @@ -59,4 +59,4 @@ export async function POST(req: NextRequest) { } } -export const runtime = "experimental-edge"; +export const runtime = "edge"; diff --git a/app/api/config/route.ts b/app/api/config/route.ts index e04e22a..65290a4 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -19,3 +19,5 @@ export async function POST(req: NextRequest) { needCode: serverConfig.needCode, }); } + +export const runtime = "edge"; diff --git a/app/api/openai/route.ts b/app/api/openai/route.ts index bed70d9..d49027c 100644 --- a/app/api/openai/route.ts +++ b/app/api/openai/route.ts @@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) { }, { status: 500, - } + }, ); } } @@ -30,4 +30,4 @@ export async function GET(req: NextRequest) { return makeRequest(req); } -export const runtime = "experimental-edge"; +export const runtime = "edge"; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 867fbc4..a352952 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -29,7 +29,6 @@ import { createMessage, useAccessStore, Theme, - ModelType, useAppConfig, ModelConfig, DEFAULT_TOPIC, @@ -57,7 +56,8 @@ import { Input, List, ListItem, Modal, Popover, showModal } from "./ui-lib"; import { useNavigate } from "react-router-dom"; import { Path } from "../constant"; import { ModelConfigList } from "./model-config"; -import { AvatarPicker } from "./emoji"; +import { Avatar, AvatarPicker } from "./emoji"; + const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), { @@ -65,10 +65,6 @@ const Markdown = dynamic( }, ); -const Avatar = dynamic(async () => (await import("./emoji")).Avatar, { - loading: () => , -}); - function exportMessages(messages: Message[], topic: string) { const mdText = `# ${topic}\n\n` + @@ -112,8 +108,6 @@ function ContextPrompts() { const session = chatStore.currentSession(); const context = session.context; - const [showPicker, setShowPicker] = useState(false); - const addContextPrompt = (prompt: Message) => { chatStore.updateCurrentSession((session) => { session.context.push(prompt); @@ -190,56 +184,15 @@ function ContextPrompts() { />
- - - - chatStore.updateCurrentSession( - (session) => (session.avatar = emoji), - ) - } - > - } - open={showPicker} - onClose={() => setShowPicker(false)} - > -
setShowPicker(true)}> - {session.avatar ? ( - - ) : ( - - )} -
-
-
- - - chatStore.updateCurrentSession( - (session) => (session.topic = e.currentTarget.value), - ) - } - > - - -
); } export function SessionConfigModel(props: { onClose: () => void }) { const chatStore = useChatStore(); - const config = useAppConfig(); const session = chatStore.currentSession(); - const context = session.context; + + const [showPicker, setShowPicker] = useState(false); const updateConfig = (updater: (config: ModelConfig) => void) => { const config = { ...session.modelConfig }; @@ -273,10 +226,59 @@ export function SessionConfigModel(props: { onClose: () => void }) { > - + + + + chatStore.updateCurrentSession( + (session) => (session.avatar = emoji), + ) + } + > + } + open={showPicker} + onClose={() => setShowPicker(false)} + > +
setShowPicker(true)}> + {session.avatar ? ( + + ) : ( + + )} +
+
+
+ + + chatStore.updateCurrentSession( + (session) => (session.topic = e.currentTarget.value), + ) + } + > + +
+ + + + + {session.modelConfig.sendMemory ? ( + + ) : ( + <> + )} +
); diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 2b6d59f..112e6b2 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -10,7 +10,7 @@ export function ModelConfigList(props: { updateConfig: (updater: (config: ModelConfig) => void) => void; }) { return ( - + <> + +
+ {masks.map((masks, i) => ( +
+ {masks.map((mask, index) => ( + + ))} +
+ ))} +
+
+ ); +} diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 1e35964..8b53419 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -134,7 +134,7 @@ export function SideBar(props: { className?: string }) { icon={} text={shouldNarrow ? undefined : Locale.Home.NewChat} onClick={() => { - chatStore.newSession(); + navigate(Path.NewChat); }} shadow /> diff --git a/app/constant.ts b/app/constant.ts index 43ae4cc..60bb73b 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -11,6 +11,11 @@ export enum Path { Home = "/", Chat = "/chat", Settings = "/settings", + NewChat = "/new-chat", +} + +export enum SlotID { + AppBody = "app-body", } export const MAX_SIDEBAR_WIDTH = 500; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 2e35cb3..0b8a467 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -3,7 +3,8 @@ import { SubmitKey } from "../store/config"; const cn = { WIP: "该功能仍在开发中……", Error: { - Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。", + Unauthorized: + "现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。", }, ChatItem: { ChatItemCount: (count: number) => `${count} 条对话`, @@ -141,7 +142,7 @@ const cn = { Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", - SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码", + SubTitle: "值越大,回复越随机", }, MaxTokens: { Title: "单次回复限制 (max_tokens)", diff --git a/app/locales/index.ts b/app/locales/index.ts index 389304f..2ce5926 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -19,7 +19,7 @@ export const AllLangs = [ "jp", "de", ] as const; -type Lang = (typeof AllLangs)[number]; +export type Lang = (typeof AllLangs)[number]; const LANG_KEY = "lang"; diff --git a/app/masks.ts b/app/masks.ts new file mode 100644 index 0000000..213d9a4 --- /dev/null +++ b/app/masks.ts @@ -0,0 +1,3 @@ +import { Mask } from "./store/mask"; + +export const BUILT_IN_MASKS: Mask[] = []; diff --git a/app/store/mask.ts b/app/store/mask.ts new file mode 100644 index 0000000..168761c --- /dev/null +++ b/app/store/mask.ts @@ -0,0 +1,81 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { getLang, Lang } from "../locales"; +import { Message } from "./chat"; +import { ModelConfig, useAppConfig } from "./config"; + +export const MASK_KEY = "mask-store"; + +export type Mask = { + id: number; + avatar: string; + name: string; + context: Message[]; + config: ModelConfig; + lang: Lang; +}; + +export const DEFAULT_MASK_STATE = { + masks: {} as Record, + globalMaskId: 0, +}; + +export type MaskState = typeof DEFAULT_MASK_STATE; +type MaskStore = MaskState & { + create: (mask: Partial) => Mask; + update: (id: number, updater: (mask: Mask) => void) => void; + delete: (id: number) => void; + search: (text: string) => Mask[]; + getAll: () => Mask[]; +}; + +export const useMaskStore = create()( + persist( + (set, get) => ({ + ...DEFAULT_MASK_STATE, + + create(mask) { + set(() => ({ globalMaskId: get().globalMaskId + 1 })); + const id = get().globalMaskId; + const masks = get().masks; + masks[id] = { + id, + avatar: "1f916", + name: "", + config: useAppConfig.getState().modelConfig, + context: [], + lang: getLang(), + ...mask, + }; + + set(() => ({ masks })); + + return masks[id]; + }, + update(id, updater) { + const masks = get().masks; + const mask = masks[id]; + if (!mask) return; + const updateMask = { ...mask }; + updater(updateMask); + masks[id] = updateMask; + set(() => ({ masks })); + }, + delete(id) { + const masks = get().masks; + delete masks[id]; + set(() => ({ masks })); + }, + getAll() { + return Object.values(get().masks).sort((a, b) => a.id - b.id); + }, + search(text) { + return Object.values(get().masks); + }, + }), + { + name: MASK_KEY, + version: 2, + }, + ), +); diff --git a/app/styles/globals.scss b/app/styles/globals.scss index c5ffacb..549f254 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -336,3 +336,9 @@ pre { box-shadow: var(--card-shadow); border-radius: 10px; } + +.one-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} From 708c6829f79d5c899a5d35d1bda6ca28c7bcad6c Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Mon, 24 Apr 2023 01:17:28 +0800 Subject: [PATCH 029/111] fixup --- app/components/home.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/home.tsx b/app/components/home.tsx index c15b995..4e33480 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -23,7 +23,6 @@ import { } from "react-router-dom"; import { SideBar } from "./sidebar"; import { useAppConfig } from "../store/config"; -import { NewChat } from "./new-chat"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -42,6 +41,10 @@ const Chat = dynamic(async () => (await import("./chat")).Chat, { loading: () => , }); +const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, { + loading: () => , +}); + export function useSwitchTheme() { const config = useAppConfig(); From e5e2f6c2e1c293efcb0b41b254f4cd12ec374440 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Sun, 23 Apr 2023 00:57:55 +0800 Subject: [PATCH 030/111] Improve tw locale --- app/locales/tw.ts | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 20e41f4..26791c7 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -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,13 +121,13 @@ const tw: LocaleType = { }, Token: { Title: "API Key", - SubTitle: "使用自己的 Key 可規避授權訪問限制", + SubTitle: "使用自己的 Key 可規避授權存取限制", Placeholder: "OpenAI API Key", }, Usage: { Title: "帳戶餘額", SubTitle(used: any, total: any) { - return `本月已使用 $${used},订阅总额 $${total}`; + return `本月已使用 $${used},訂閱總額 $${total}`; }, IsChecking: "正在檢查…", Check: "重新檢查", @@ -135,17 +135,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)", @@ -164,16 +164,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: "新增一條", }, }; From ffa73025716774b88c685ef21c6a2e6d137b597f Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 25 Apr 2023 00:49:27 +0800 Subject: [PATCH 031/111] feat: add mask page --- app/api/openai/typing.ts | 2 + app/components/button.tsx | 17 +- app/components/chat.tsx | 203 ++++--------------- app/components/home.module.scss | 3 - app/components/home.tsx | 5 + app/components/mask.module.scss | 33 ++++ app/components/mask.tsx | 258 +++++++++++++++++++++++++ app/components/model-config.tsx | 2 +- app/components/new-chat.module.scss | 25 ++- app/components/new-chat.tsx | 17 +- app/components/settings.module.scss | 2 - app/components/settings.tsx | 16 +- app/components/ui-lib.module.scss | 23 ++- app/components/ui-lib.tsx | 27 ++- app/config/masks.ts | 3 + app/constant.ts | 1 + app/icons/left.svg | 1 + app/requests.ts | 2 +- app/store/chat.ts | 25 +-- app/store/mask.ts | 24 ++- app/styles/globals.scss | 3 + app/{components => styles}/window.scss | 0 22 files changed, 460 insertions(+), 232 deletions(-) create mode 100644 app/components/mask.module.scss create mode 100644 app/components/mask.tsx create mode 100644 app/config/masks.ts create mode 100644 app/icons/left.svg rename app/{components => styles}/window.scss (100%) diff --git a/app/api/openai/typing.ts b/app/api/openai/typing.ts index b936530..2286d23 100644 --- a/app/api/openai/typing.ts +++ b/app/api/openai/typing.ts @@ -5,3 +5,5 @@ import type { export type ChatRequest = CreateChatCompletionRequest; export type ChatResponse = CreateChatCompletionResponse; + +export type Updater = (updater: (value: T) => void) => void; diff --git a/app/components/button.tsx b/app/components/button.tsx index 1675a4b..3a2cb8a 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -4,7 +4,7 @@ import styles from "./button.module.scss"; export function IconButton(props: { onClick?: () => void; - icon: JSX.Element; + icon?: JSX.Element; text?: string; bordered?: boolean; shadow?: boolean; @@ -26,11 +26,16 @@ export function IconButton(props: { disabled={props.disabled} role="button" > -
- {props.icon} -
+ {props.icon && ( +
+ {props.icon} +
+ )} + {props.text && (
{props.text}
)} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index b38b083..24da322 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -57,6 +57,8 @@ import { useNavigate } from "react-router-dom"; import { Path } from "../constant"; import { ModelConfigList } from "./model-config"; import { Avatar, AvatarPicker } from "./emoji"; +import { MaskConfig } from "./mask"; +import { DEFAULT_MASK_ID } from "../store/mask"; const Markdown = dynamic( async () => memo((await import("./markdown")).Markdown), @@ -103,103 +105,10 @@ function exportMessages(messages: Message[], topic: string) { }); } -function ContextPrompts() { - const chatStore = useChatStore(); - const session = chatStore.currentSession(); - const context = session.context; - - const addContextPrompt = (prompt: Message) => { - chatStore.updateCurrentSession((session) => { - session.context.push(prompt); - }); - }; - - const removeContextPrompt = (i: number) => { - chatStore.updateCurrentSession((session) => { - session.context.splice(i, 1); - }); - }; - - const updateContextPrompt = (i: number, prompt: Message) => { - chatStore.updateCurrentSession((session) => { - session.context[i] = prompt; - }); - }; - - return ( - <> -
- {context.map((c, i) => ( -
- - - updateContextPrompt(i, { - ...c, - content: e.currentTarget.value as any, - }) - } - /> - } - className={chatStyle["context-delete-button"]} - onClick={() => removeContextPrompt(i)} - bordered - /> -
- ))} - -
- } - text={Locale.Context.Add} - bordered - className={chatStyle["context-prompt-button"]} - onClick={() => - addContextPrompt({ - role: "system", - content: "", - date: "", - }) - } - /> -
-
- - ); -} - export function SessionConfigModel(props: { onClose: () => void }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); - const [showPicker, setShowPicker] = useState(false); - - const updateConfig = (updater: (config: ModelConfig) => void) => { - const config = { ...session.modelConfig }; - updater(config); - chatStore.updateCurrentSession((session) => (session.modelConfig = config)); - }; - return (
void }) { key="reset" icon={} bordered - text="重置预设" + text="重置" onClick={() => confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession() } @@ -219,69 +128,29 @@ export function SessionConfigModel(props: { onClose: () => void }) { key="copy" icon={} bordered - text="保存预设" + text="保存为面具" onClick={() => copyToClipboard(session.memoryPrompt)} />, ]} > - - - - - - chatStore.updateCurrentSession( - (session) => (session.avatar = emoji), - ) - } - > - } - open={showPicker} - onClose={() => setShowPicker(false)} - > -
setShowPicker(true)} - style={{ cursor: "pointer" }} - > - {session.avatar ? ( - - ) : ( - - )} -
-
-
- - - chatStore.updateCurrentSession( - (session) => (session.topic = e.currentTarget.value), - ) - } - > - -
- - - - - {session.modelConfig.sendMemory ? ( - - ) : ( - <> - )} - + { + const mask = { ...session.mask }; + updater(mask); + chatStore.updateCurrentSession((session) => (session.mask = mask)); + }} + extraListItems={ + session.mask.modelConfig.sendMemory ? ( + + ) : ( + <> + ) + } + >
); @@ -294,7 +163,7 @@ function PromptToast(props: { }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); - const context = session.context; + const context = session.mask.context; return (
@@ -617,7 +486,7 @@ export function Chat() { inputRef.current?.focus(); }; - const context: RenderMessage[] = session.context.slice(); + const context: RenderMessage[] = session.mask.context.slice(); const accessStore = useAccessStore(); @@ -680,20 +549,20 @@ export function Chat() { return (
-
-
+
+
{!session.topic ? DEFAULT_TOPIC : session.topic}
-
+
{Locale.Chat.SubTitle(session.messages.length)}
-
-
+
+
} bordered @@ -701,14 +570,14 @@ export function Chat() { onClick={() => navigate(Path.Home)} />
-
+
} bordered onClick={renameSession} />
-
+
} bordered @@ -722,7 +591,7 @@ export function Chat() { />
{!isMobileScreen && ( -
+
: } bordered @@ -773,10 +642,10 @@ export function Chat() {
{message.role === "user" ? ( - ) : session.avatar ? ( - - ) : ( + ) : session.mask.id === DEFAULT_MASK_ID ? ( + ) : ( + )}
{showTyping && ( diff --git a/app/components/home.module.scss b/app/components/home.module.scss index b845253..470bc9d 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -1,6 +1,3 @@ -@import "./window.scss"; -@import "../styles/animation.scss"; - @mixin container { background-color: var(--white); border: var(--border-in-light); diff --git a/app/components/home.tsx b/app/components/home.tsx index 4e33480..a83a779 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -45,6 +45,10 @@ const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, { loading: () => , }); +const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, { + loading: () => , +}); + export function useSwitchTheme() { const config = useAppConfig(); @@ -109,6 +113,7 @@ function Screen() { } /> } /> + } /> } /> } /> diff --git a/app/components/mask.module.scss b/app/components/mask.module.scss new file mode 100644 index 0000000..dc82325 --- /dev/null +++ b/app/components/mask.module.scss @@ -0,0 +1,33 @@ +.mask-page { + height: 100%; + display: flex; + flex-direction: column; + + .mask-page-body { + padding: 20px; + overflow-y: auto; + + .search-bar { + width: 100%; + max-width: 100%; + margin-bottom: 20px; + } + + .mask-item { + .mask-icon { + display: flex; + align-items: center; + justify-content: center; + border: var(--border-in-light); + border-radius: 10px; + padding: 6px; + } + + .mask-actions { + display: flex; + flex-wrap: nowrap; + transition: all ease 0.3s; + } + } + } +} diff --git a/app/components/mask.tsx b/app/components/mask.tsx new file mode 100644 index 0000000..281a3d3 --- /dev/null +++ b/app/components/mask.tsx @@ -0,0 +1,258 @@ +import { IconButton } from "./button"; +import { ErrorBoundary } from "./error"; + +import styles from "./mask.module.scss"; + +import DownloadIcon from "../icons/download.svg"; +import EditIcon from "../icons/edit.svg"; +import AddIcon from "../icons/add.svg"; +import CloseIcon from "../icons/close.svg"; +import DeleteIcon from "../icons/delete.svg"; +import CopyIcon from "../icons/copy.svg"; + +import { DEFAULT_MASK_AVATAR, DEFAULT_MASK_ID, Mask } from "../store/mask"; +import { + Message, + ModelConfig, + ROLES, + useAppConfig, + useChatStore, +} from "../store"; +import { Input, List, ListItem, Modal, Popover } from "./ui-lib"; +import { Avatar, AvatarPicker, EmojiAvatar } from "./emoji"; +import Locale from "../locales"; +import { useNavigate } from "react-router-dom"; + +import chatStyle from "./chat.module.scss"; +import { useState } from "react"; +import { copyToClipboard } from "../utils"; +import { Updater } from "../api/openai/typing"; +import { ModelConfigList } from "./model-config"; + +export function MaskConfig(props: { + mask: Mask; + updateMask: Updater; + extraListItems?: JSX.Element; +}) { + const [showPicker, setShowPicker] = useState(false); + + const updateConfig = (updater: (config: ModelConfig) => void) => { + const config = { ...props.mask.modelConfig }; + updater(config); + props.updateMask((mask) => (mask.modelConfig = config)); + }; + + return ( + <> + { + const context = props.mask.context.slice(); + updater(context); + props.updateMask((mask) => (mask.context = context)); + }} + /> + + + + { + props.updateMask((mask) => (mask.avatar = emoji)); + setShowPicker(false); + }} + > + } + open={showPicker} + onClose={() => setShowPicker(false)} + > +
setShowPicker(true)} + style={{ cursor: "pointer" }} + > + {props.mask.avatar !== DEFAULT_MASK_AVATAR ? ( + + ) : ( + + )} +
+
+
+ + + props.updateMask((mask) => (mask.name = e.currentTarget.value)) + } + > + +
+ + + + {props.extraListItems} + + + ); +} + +export function ContextPrompts(props: { + context: Message[]; + updateContext: (updater: (context: Message[]) => void) => void; +}) { + const context = props.context; + + const addContextPrompt = (prompt: Message) => { + props.updateContext((context) => context.push(prompt)); + }; + + const removeContextPrompt = (i: number) => { + props.updateContext((context) => context.splice(i, 1)); + }; + + const updateContextPrompt = (i: number, prompt: Message) => { + props.updateContext((context) => (context[i] = prompt)); + }; + + return ( + <> +
+ {context.map((c, i) => ( +
+ + + updateContextPrompt(i, { + ...c, + content: e.currentTarget.value as any, + }) + } + /> + } + className={chatStyle["context-delete-button"]} + onClick={() => removeContextPrompt(i)} + bordered + /> +
+ ))} + +
+ } + text={Locale.Context.Add} + bordered + className={chatStyle["context-prompt-button"]} + onClick={() => + addContextPrompt({ + role: "system", + content: "", + date: "", + }) + } + /> +
+
+ + ); +} + +export function MaskPage() { + const config = useAppConfig(); + const navigate = useNavigate(); + const masks: Mask[] = new Array(10).fill(0).map((m, i) => ({ + id: i, + avatar: "1f606", + name: "预设角色 " + i.toString(), + context: [ + { role: "assistant", content: "你好,有什么可以帮忙的吗", date: "" }, + ], + modelConfig: config.modelConfig, + lang: "cn", + })); + + return ( + +
+
+
+
预设角色面具
+
编辑预设角色定义
+
+ +
+
+ } bordered /> +
+
+ } bordered /> +
+
+ } + bordered + onClick={() => navigate(-1)} + /> +
+
+
+ +
+ + + + {masks.map((m) => ( + + +
+ } + className={styles["mask-item"]} + > +
+ } text="对话" /> + } text="编辑" /> + } text="删除" /> +
+ + ))} + +
+
+ + ); +} diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 797bcb3..32c2f5c 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -97,7 +97,7 @@ export function ModelConfigList(props: { title={props.modelConfig.historyMessageCount.toString()} value={props.modelConfig.historyMessageCount} min="0" - max="25" + max="32" step="1" onChange={(e) => props.updateConfig( diff --git a/app/components/new-chat.module.scss b/app/components/new-chat.module.scss index 9cd1796..1fdd86a 100644 --- a/app/components/new-chat.module.scss +++ b/app/components/new-chat.module.scss @@ -1,3 +1,5 @@ +@import "../styles/animation.scss"; + .new-chat { height: 100%; width: 100%; @@ -5,11 +7,21 @@ align-items: center; justify-content: center; flex-direction: column; - padding-top: 80px; + + .mask-header { + display: flex; + justify-content: space-between; + width: 100%; + padding: 10px; + box-sizing: border-box; + animation: slide-in-from-top ease 0.3s; + } .mask-cards { display: flex; + margin-top: 5vh; margin-bottom: 20px; + animation: slide-in ease 0.3s; .mask-card { padding: 20px 10px; @@ -32,15 +44,18 @@ .title { font-size: 32px; font-weight: bolder; - animation: slide-in ease 0.3s; + margin-bottom: 1vh; + animation: slide-in ease 0.35s; } .sub-title { - animation: slide-in ease 0.3s; + animation: slide-in ease 0.4s; } .search-bar { - margin-top: 20px; + margin-top: 5vh; + margin-bottom: 5vh; + animation: slide-in ease 0.45s; } .masks { @@ -50,7 +65,7 @@ align-items: center; padding-top: 20px; - animation: slide-in ease 0.3s; + animation: slide-in ease 0.5s; .mask-row { margin-bottom: 10px; diff --git a/app/components/new-chat.tsx b/app/components/new-chat.tsx index e053a7f..a161ee0 100644 --- a/app/components/new-chat.tsx +++ b/app/components/new-chat.tsx @@ -1,7 +1,10 @@ import { useEffect, useRef } from "react"; import { SlotID } from "../constant"; +import { IconButton } from "./button"; import { EmojiAvatar } from "./emoji"; import styles from "./new-chat.module.scss"; +import LeftIcon from "../icons/left.svg"; +import { useNavigate } from "react-router-dom"; function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { const xmin = Math.max(aRect.x, bRect.x); @@ -59,8 +62,18 @@ export function NewChat() { })), ); + const navigate = useNavigate(); + return (
+
+ } + text="返回" + onClick={() => navigate(-1)} + > + +
@@ -74,7 +87,9 @@ export function NewChat() {
挑选一个面具
-
现在开始,与面具背后的思维碰撞
+
+ 现在开始,与面具背后的灵魂思维碰撞 +
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 6fb5a68..30abc36 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -1,5 +1,3 @@ -@import "./window.scss"; - .settings { padding: 20px; overflow: auto; diff --git a/app/components/settings.tsx b/app/components/settings.tsx index f396ed3..83aec5a 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -202,17 +202,17 @@ export function Settings() { return ( -
-
-
+
+
+
{Locale.Settings.Title}
-
+
{Locale.Settings.SubTitle}
-
-
+
+
} onClick={() => { @@ -227,7 +227,7 @@ export function Settings() { title={Locale.Settings.Actions.ClearAll} />
-
+
} onClick={() => { @@ -242,7 +242,7 @@ export function Settings() { title={Locale.Settings.Actions.ResetAll} />
-
+
} onClick={() => navigate(Path.Home)} diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index e3acd6d..b20edc3 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -36,14 +36,23 @@ padding: 10px 20px; animation: slide-in ease 0.6s; - .list-item-title { - font-size: 14px; - font-weight: bolder; - } + .list-header { + display: flex; + align-items: center; - .list-item-sub-title { - font-size: 12px; - font-weight: normal; + .list-icon { + margin-right: 10px; + } + + .list-item-title { + font-size: 14px; + font-weight: bolder; + } + + .list-item-sub-title { + font-size: 12px; + font-weight: normal; + } } } diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index 4a92461..5b6ed95 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -37,21 +37,34 @@ export function ListItem(props: { title: string; subTitle?: string; children?: JSX.Element | JSX.Element[]; + icon?: JSX.Element; + className?: string; }) { return ( -
-
-
{props.title}
- {props.subTitle && ( -
{props.subTitle}
- )} +
+
+ {props.icon &&
{props.icon}
} +
+
{props.title}
+ {props.subTitle && ( +
+ {props.subTitle} +
+ )} +
{props.children}
); } -export function List(props: { children: JSX.Element[] | JSX.Element }) { +export function List(props: { + children: + | Array + | JSX.Element + | null + | undefined; +}) { return
{props.children}
; } diff --git a/app/config/masks.ts b/app/config/masks.ts new file mode 100644 index 0000000..fc635d9 --- /dev/null +++ b/app/config/masks.ts @@ -0,0 +1,3 @@ +import { Mask } from "../store/mask"; + +export const BUILTIN_MASKS: Mask[] = []; diff --git a/app/constant.ts b/app/constant.ts index 60bb73b..9be260a 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -12,6 +12,7 @@ export enum Path { Chat = "/chat", Settings = "/settings", NewChat = "/new-chat", + Masks = "/masks", } export enum SlotID { diff --git a/app/icons/left.svg b/app/icons/left.svg new file mode 100644 index 0000000..8f1cf52 --- /dev/null +++ b/app/icons/left.svg @@ -0,0 +1 @@ + diff --git a/app/requests.ts b/app/requests.ts index 6ab075a..7e92cc4 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -30,7 +30,7 @@ const makeRequestParam = ( const modelConfig = { ...useAppConfig.getState().modelConfig, - ...useChatStore.getState().currentSession().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, }; // override model config diff --git a/app/store/chat.ts b/app/store/chat.ts index 4692a5a..a95d767 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -12,6 +12,7 @@ import { isMobileScreen, trimTopic } from "../utils"; import Locale from "../locales"; import { showToast } from "../components/ui-lib"; import { DEFAULT_CONFIG, ModelConfig, ModelType, useAppConfig } from "./config"; +import { createEmptyMask, Mask } from "./mask"; export type Message = ChatCompletionResponseMessage & { date: string; @@ -41,16 +42,16 @@ export interface ChatStat { export interface ChatSession { id: number; + topic: string; - avatar?: string; + memoryPrompt: string; - context: Message[]; messages: Message[]; stat: ChatStat; lastUpdate: string; lastSummarizeIndex: number; - modelConfig: ModelConfig; + mask: Mask; } export const DEFAULT_TOPIC = Locale.Store.DefaultTopic; @@ -66,7 +67,6 @@ function createEmptySession(): ChatSession { id: Date.now(), topic: DEFAULT_TOPIC, memoryPrompt: "", - context: [], messages: [], stat: { tokenCount: 0, @@ -75,8 +75,7 @@ function createEmptySession(): ChatSession { }, lastUpdate: createDate, lastSummarizeIndex: 0, - - modelConfig: useAppConfig.getState().modelConfig, + mask: createEmptyMask(), }; } @@ -322,11 +321,11 @@ export const useChatStore = create()( const messages = session.messages.filter((msg) => !msg.isError); const n = messages.length; - const context = session.context.slice(); + const context = session.mask.context.slice(); // long term memory if ( - session.modelConfig.sendMemory && + session.mask.modelConfig.sendMemory && session.memoryPrompt && session.memoryPrompt.length > 0 ) { @@ -432,7 +431,7 @@ export const useChatStore = create()( if ( historyMsgLength > config.modelConfig.compressMessageLengthThreshold && - session.modelConfig.sendMemory + session.mask.modelConfig.sendMemory ) { requestChatStream( toBeSummarizedMsgs.concat({ @@ -485,14 +484,8 @@ export const useChatStore = create()( migrate(persistedState, version) { const state = persistedState as ChatStore; - if (version === 1) { - state.sessions.forEach((s) => (s.context = [])); - } - if (version < 2) { - state.sessions.forEach( - (s) => (s.modelConfig = { ...DEFAULT_CONFIG.modelConfig }), - ); + state.sessions.forEach((s) => (s.mask = createEmptyMask())); } return state; diff --git a/app/store/mask.ts b/app/store/mask.ts index 168761c..9a9d985 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -1,8 +1,8 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { getLang, Lang } from "../locales"; -import { Message } from "./chat"; -import { ModelConfig, useAppConfig } from "./config"; +import { DEFAULT_TOPIC, Message } from "./chat"; +import { ModelConfig, ModelType, useAppConfig } from "./config"; export const MASK_KEY = "mask-store"; @@ -11,7 +11,7 @@ export type Mask = { avatar: string; name: string; context: Message[]; - config: ModelConfig; + modelConfig: ModelConfig; lang: Lang; }; @@ -29,6 +29,18 @@ type MaskStore = MaskState & { getAll: () => Mask[]; }; +export const DEFAULT_MASK_ID = 1145141919810; +export const DEFAULT_MASK_AVATAR = "gpt-bot"; +export const createEmptyMask = () => + ({ + id: DEFAULT_MASK_ID, + avatar: DEFAULT_MASK_AVATAR, + name: DEFAULT_TOPIC, + context: [], + modelConfig: useAppConfig.getState().modelConfig, + lang: getLang(), + } as Mask); + export const useMaskStore = create()( persist( (set, get) => ({ @@ -39,12 +51,8 @@ export const useMaskStore = create()( const id = get().globalMaskId; const masks = get().masks; masks[id] = { + ...createEmptyMask(), id, - avatar: "1f916", - name: "", - config: useAppConfig.getState().modelConfig, - context: [], - lang: getLang(), ...mask, }; diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 549f254..9caf663 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -1,3 +1,6 @@ +@import "./animation.scss"; +@import "./window.scss"; + @mixin light { --theme: light; diff --git a/app/components/window.scss b/app/styles/window.scss similarity index 100% rename from app/components/window.scss rename to app/styles/window.scss From c4ca05865d87533614de03989788887e3d4cafbf Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 25 Apr 2023 20:12:00 +0800 Subject: [PATCH 032/111] Update README_CN.md --- README_CN.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README_CN.md b/README_CN.md index 03ec2a1..23dfee8 100644 --- a/README_CN.md +++ b/README_CN.md @@ -114,14 +114,16 @@ OPENAI_API_KEY= ### 本地开发 -1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT; -2. 执行 `yarn install && yarn dev` 即可。 +1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT; +2. 执行 `yarn install && yarn dev` 即可。⚠️注意:此命令仅用于本地开发,不要用于部署! +3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。 ## 部署 ### 容器部署 (推荐) +> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。 -> 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。 +> ⚠️注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。 ```shell docker pull yidadaa/chatgpt-next-web @@ -143,6 +145,8 @@ docker run -d -p 3000:3000 \ yidadaa/chatgpt-next-web ``` +如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。 + ### 本地部署 在控制台运行下方命令: @@ -151,6 +155,8 @@ docker run -d -p 3000:3000 \ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh) ``` +⚠️注意:如果你安装过程中遇到了问题,请使用 docker 部署。 + ## 鸣谢 ### 捐赠者 From 2e01a93a4a9195db095d0d208bf9c788cacb8294 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 25 Apr 2023 22:54:21 +0800 Subject: [PATCH 033/111] Update issue templates --- .github/ISSUE_TEMPLATE/反馈问题.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/反馈问题.md b/.github/ISSUE_TEMPLATE/反馈问题.md index ea56aa6..b21442f 100644 --- a/.github/ISSUE_TEMPLATE/反馈问题.md +++ b/.github/ISSUE_TEMPLATE/反馈问题.md @@ -8,6 +8,9 @@ assignees: '' --- **反馈须知** + +⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭。 + > 请在下方中括号内输入 x 来表示你已经知晓相关内容。 - [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; - [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 From a7a8aad9bc584f3bac0aa27eb8d295381939995b Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 26 Apr 2023 02:02:46 +0800 Subject: [PATCH 034/111] feat: add mask crud --- app/components/chat-list.tsx | 4 +- app/components/chat.tsx | 36 ++++++-- app/components/emoji.tsx | 6 +- app/components/mask.module.scss | 76 +++++++++++++++-- app/components/mask.tsx | 126 +++++++++++++++++++--------- app/components/new-chat.module.scss | 13 +-- app/components/new-chat.tsx | 84 ++++++++++++++----- app/icons/dark.svg | 2 +- app/icons/light.svg | 2 +- app/icons/mask.svg | 1 + app/icons/prompt.svg | 1 + app/store/chat.ts | 26 ++++-- app/store/mask.ts | 9 +- app/styles/globals.scss | 4 +- app/utils.ts | 24 ++++-- 15 files changed, 313 insertions(+), 101 deletions(-) create mode 100644 app/icons/mask.svg create mode 100644 app/icons/prompt.svg diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 637e0b1..6ae274f 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -57,7 +57,9 @@ export function ChatItem(props: {
{Locale.ChatItem.ChatItemCount(props.count)}
-
{props.time}
+
+ {new Date(props.time).toLocaleString()} +
)} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 24da322..d8f3e8f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -9,8 +9,8 @@ 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 AddIcon from "../icons/add.svg"; -import DeleteIcon from "../icons/delete.svg"; +import PromptIcon from "../icons/prompt.svg"; +import MaskIcon from "../icons/mask.svg"; import MaxIcon from "../icons/max.svg"; import MinIcon from "../icons/min.svg"; @@ -261,9 +261,11 @@ function useScrollToBottom() { export function ChatActions(props: { showPromptModal: () => void; scrollToBottom: () => void; + showPromptHints: () => void; hitBottom: boolean; }) { const config = useAppConfig(); + const navigate = useNavigate(); // switch themes const theme = config.theme; @@ -318,6 +320,22 @@ export function ChatActions(props: { ) : null}
+ +
+ +
+ +
{ + navigate(Path.Masks); + }} + > + +
); } @@ -360,9 +378,9 @@ export function Chat() { ); const onPromptSelect = (prompt: Prompt) => { - setUserInput(prompt.content); setPromptHints([]); inputRef.current?.focus(); + setUserInput(prompt.content); }; // auto grow input @@ -723,6 +741,10 @@ export function Chat() { showPromptModal={() => setShowPromptModal(true)} scrollToBottom={scrollToBottom} hitBottom={hitBottom} + showPromptHints={() => { + inputRef.current?.focus(); + onSearch(""); + }} />