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 5dce8fd6..d27c138e 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 00000000..3aad2add --- /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 8d875fee..5af4374c 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 1764f2db..e9ec7e68 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 d2d64aa0..03ec2a10 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 5af4374c..0a3ff865 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 3cb838e6..9159f1cf 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 0a3ff865..fe2a07da 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 d27c138e..02f46141 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 cab8812c..f0139205 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 02f46141..c6bc61eb 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 828b7576..ef30243c 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 f0139205..fb0f740c 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 c6bc61eb..bab42298 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 d81b5b35..02148492 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 00000000..338dec19 --- /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 6f08ad75..68744546 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 9d6e9062..0af9fef5 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 19047ad1..78500551 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 342ea4a4..b7d9f830 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 efca5c0b..8b5e1d74 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 0af9fef5..dfec8d3e 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 fb0f740c..626336af 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 9bf0d571..38e755bc 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 8b5e1d74..123be03a 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 338dec19..71e75f8a 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 68744546..43ae4cc6 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 41f13549..22550e39 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 0ac94bdd..bed70d92 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 b9653339..751a9ac1 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 02148492..4b552e28 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 9159f1cf..ce72bb7c 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 fe2a07da..89995e0b 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 a72aa868..ffc05cf8 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 38e755bc..1c021d88 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 123be03a..ac3ce90e 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 bab42298..c5cc5429 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 ac3ce90e..32334028 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 71e75f8a..d0c99dd1 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 1c198195..777cea59 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 e71abfaf..42a4c8f6 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 20e56960..f7af4bfb 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 e2a9eb21..efecf113 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 f0453b5c..b519ef45 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 2818820b..1c8d66d9 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 04a84624..86f1f417 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 44c07898..20e41f47 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 ce72bb7c..0e757090 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 89995e0b..2294130a 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 00000000..346f38da --- /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 3bdb58ca..7b7bbd04 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 626336af..637e0b11 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 1c021d88..b0b44d9c 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 d0c99dd1..1e35964d 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 c5cc5429..b80bf5a1 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 b0b44d9c..7476c08f 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 32334028..851dba1a 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 b7f09558..9df76d32 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 ae412870..1b2b4c7f 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 ffc05cf8..8e04db3a 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 2294130a..652e26f5 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 346f38da..93733409 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 37c66228..5815d741 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 e9ec7e68..a6d1e3d3 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 7f6818d3..9dc72626 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 9dc72626..689377ca 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 7cd2889f..3a1be391 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 c5cc5429..867fbc49 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 00000000..b1d092a5 --- /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 7476c08f..be630e1f 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 00000000..2b6d59f5 --- /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 9df76d32..6fb5a68b 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 1b2b4c7f..ffe540a9 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 8965c06a..e3acd6d6 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 8e04db3a..4a92461c 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 777cea59..2e35cb30 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 652e26f5..fcea406b 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 93733409..1e604607 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 7b7bbd04..0760f48c 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 5815d741..c5ffacbc 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 dfec8d3e..0ebcc93a 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 22550e39..2775ff06 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 e04e22a0..65290a47 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 bed70d92..d49027c6 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 867fbc49..a3529527 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 2b6d59f5..112e6b2e 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 1e35964d..8b534192 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 43ae4cc6..60bb73bd 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 2e35cb30..0b8a467b 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 389304f8..2ce59261 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 00000000..213d9a47 --- /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 00000000..168761cc --- /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 c5ffacbc..549f254b 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 c15b995e..4e334805 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 20e41f47..26791c77 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 b936530c..2286d231 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 1675a4b7..3a2cb8ac 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 b38b0835..24da3221 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 b8452534..470bc9dd 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 4e334805..a83a7798 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 00000000..dc823253 --- /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 00000000..281a3d3b --- /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 797bcb37..32c2f5c0 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 9cd17960..1fdd86a6 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 e053a7fe..a161ee01 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 6fb5a68b..30abc36d 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 f396ed32..83aec5af 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 e3acd6d6..b20edc35 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 4a92461c..5b6ed959 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 00000000..fc635d9c --- /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 60bb73bd..9be260a7 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 00000000..8f1cf52d --- /dev/null +++ b/app/icons/left.svg @@ -0,0 +1 @@ + diff --git a/app/requests.ts b/app/requests.ts index 6ab075a8..7e92cc45 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 4692a5a4..a95d767b 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 168761cc..9a9d985e 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 549f254b..9caf663d 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 03ec2a10..23dfee85 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 ea56aa6f..b21442f5 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 637e0b11..6ae274fb 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 24da3221..d8f3e8ff 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(""); + }} />