From a3ca8ea5c458a8453c21095b65c88305125243ab Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 22 Apr 2023 00:35:50 +0800 Subject: [PATCH 01/16] 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 4cdb2f0fa37c9e97dd4dafe490955a57a5940370 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 22 Apr 2023 01:13:23 +0800 Subject: [PATCH 02/16] 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 7345639af33aede885afe6828a0969cf1f9a4a2d Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 23 Apr 2023 01:27:15 +0800 Subject: [PATCH 03/16] 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 04/16] 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 07/16] 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 ffa73025716774b88c685ef21c6a2e6d137b597f Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 25 Apr 2023 00:49:27 +0800 Subject: [PATCH 08/16] 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 a7a8aad9bc584f3bac0aa27eb8d295381939995b Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 26 Apr 2023 02:02:46 +0800 Subject: [PATCH 09/16] 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(""); + }} />