From 7345639af33aede885afe6828a0969cf1f9a4a2d Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sun, 23 Apr 2023 01:27:15 +0800 Subject: [PATCH] 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 =