From b85245e317d7fc2f48dacb9a1d65eef034502cb4 Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 2 Apr 2023 17:48:43 +0000 Subject: [PATCH] feat: #138 add context prompt, close #330 #321 --- app/components/button.module.scss | 26 +---- app/components/button.tsx | 6 +- app/components/chat-list.tsx | 6 +- app/components/chat.module.scss | 71 ++++++++++++ app/components/chat.tsx | 187 +++++++++++++++++++++++++----- app/components/home.module.scss | 1 + app/components/home.tsx | 4 +- app/components/window.scss | 3 +- app/locales/cn.ts | 2 +- app/locales/en.ts | 2 +- app/locales/es.ts | 2 +- app/locales/tw.ts | 2 +- app/store/app.ts | 37 ++++-- app/styles/globals.scss | 16 ++- 14 files changed, 296 insertions(+), 69 deletions(-) create mode 100644 app/components/chat.module.scss diff --git a/app/components/button.module.scss b/app/components/button.module.scss index b882a0c1..88da9748 100644 --- a/app/components/button.module.scss +++ b/app/components/button.module.scss @@ -6,19 +6,21 @@ justify-content: center; padding: 10px; - box-shadow: var(--card-shadow); cursor: pointer; transition: all 0.3s ease; overflow: hidden; user-select: none; } +.shadow { + box-shadow: var(--card-shadow); +} + .border { border: var(--border-in-light); } .icon-button:hover { - filter: brightness(0.9); border-color: var(--primary); } @@ -36,25 +38,7 @@ } } -@mixin dark-button { - div:not(:global(.no-dark))>.icon-button-icon { - filter: invert(0.5); - } - - .icon-button:hover { - filter: brightness(1.2); - } -} - -:global(.dark) { - @include dark-button; -} - -@media (prefers-color-scheme: dark) { - @include dark-button; -} - .icon-button-text { margin-left: 5px; font-size: 12px; -} \ No newline at end of file +} diff --git a/app/components/button.tsx b/app/components/button.tsx index 43b699b6..f40a4e8f 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -7,6 +7,7 @@ export function IconButton(props: { icon: JSX.Element; text?: string; bordered?: boolean; + shadow?: boolean; className?: string; title?: string; }) { @@ -14,10 +15,13 @@ export function IconButton(props: {
{props.icon}
{props.text && ( diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 5a74ff15..8ad2b7dc 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -11,6 +11,7 @@ import { } from "../store"; import Locale from "../locales"; +import { isMobileScreen } from "../utils"; export function ChatItem(props: { onClick?: () => void; @@ -61,7 +62,10 @@ export function ChatList() { key={i} selected={i === selectedIndex} onClick={() => selectSession(i)} - onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)} + onDelete={() => + (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) && + removeSession(i) + } /> ))}
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss new file mode 100644 index 00000000..b52baa12 --- /dev/null +++ b/app/components/chat.module.scss @@ -0,0 +1,71 @@ +.prompt-toast { + position: absolute; + bottom: -50px; + z-index: 999; + display: flex; + justify-content: center; + width: calc(100% - 40px); + + .prompt-toast-inner { + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + background-color: var(--white); + color: var(--black); + + border: var(--border-in-light); + box-shadow: var(--card-shadow); + padding: 10px 20px; + border-radius: 100px; + + .prompt-toast-content { + margin-left: 10px; + } + } +} + +.context-prompt { + .context-prompt-row { + display: flex; + justify-content: center; + width: 100%; + margin-bottom: 10px; + + .context-role { + margin-right: 10px; + } + + .context-content { + flex: 1; + max-width: 100%; + text-align: left; + } + + .context-delete-button { + margin-left: 10px; + } + } + + .context-prompt-button { + flex: 1; + } +} + +.memory-prompt { + margin-top: 20px; + + .memory-prompt-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 10px; + } + + .memory-prompt-content { + background-color: var(--gray); + border-radius: 6px; + padding: 10px; + font-size: 12px; + user-select: text; + } +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 7300549c..2294f39b 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -9,6 +9,8 @@ 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 AddIcon from "../icons/add.svg"; +import DeleteIcon from "../icons/delete.svg"; import { Message, @@ -16,6 +18,7 @@ import { useChatStore, ChatSession, BOT_HELLO, + ROLES, } from "../store"; import { @@ -33,8 +36,9 @@ import Locale from "../locales"; import { IconButton } from "./button"; import styles from "./home.module.scss"; +import chatStyle from "./chat.module.scss"; -import { showModal, showToast } from "./ui-lib"; +import { Modal, showModal, showToast } from "./ui-lib"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -94,26 +98,130 @@ function exportMessages(messages: Message[], topic: string) { }); } -function showMemoryPrompt(session: ChatSession) { - showModal({ - title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`, - children: ( -
-
-          {session.memoryPrompt || Locale.Memory.EmptyContent}
-        
+function PromptToast(props: { + showModal: boolean; + setShowModal: (_: boolean) => void; +}) { + 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 ( +
+
props.setShowModal(true)} + > + + + 已设置 {context.length} 条前置上下文 +
- ), - actions: [ - } - bordered - text={Locale.Memory.Copy} - onClick={() => copyToClipboard(session.memoryPrompt)} - />, - ], - }); + {props.showModal && ( +
+ props.setShowModal(false)} + actions={[ + } + bordered + text={Locale.Memory.Copy} + onClick={() => copyToClipboard(session.memoryPrompt)} + />, + ]} + > + <> + {" "} +
+ {context.map((c, i) => ( +
+ + + updateContextPrompt(i, { + ...c, + content: e.target.value as any, + }) + } + > + } + className={chatStyle["context-delete-button"]} + onClick={() => removeContextPrompt(i)} + /> +
+ ))} + +
+ } + text="新增" + 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} +
+
+ +
+
+ )} +
+ ); } function useSubmitHandler() { @@ -172,9 +280,8 @@ function useScrollToBottom() { // auto scroll useLayoutEffect(() => { const dom = scrollRef.current; - if (dom && autoScroll) { - dom.scrollTop = dom.scrollHeight; + setTimeout(() => (dom.scrollTop = dom.scrollHeight), 500); } }); @@ -243,8 +350,12 @@ export function Chat(props: { setPromptHints([]); } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { // check if need to trigger auto completion - if (text.startsWith("/") && text.length > 1) { - onSearch(text.slice(1)); + if (text.startsWith("/")) { + let searchText = text.slice(1); + if (searchText.length === 0) { + searchText = " "; + } + onSearch(searchText); } } }; @@ -299,8 +410,18 @@ export function Chat(props: { const config = useChatStore((state) => state.config); + const context: RenderMessage[] = session.context.slice(); + + if ( + context.length === 0 && + session.messages.at(0)?.content !== BOT_HELLO.content + ) { + context.push(BOT_HELLO); + } + // preview messages - const messages = (session.messages as RenderMessage[]) + const messages = context + .concat(session.messages as RenderMessage[]) .concat( isLoading ? [ @@ -326,6 +447,8 @@ export function Chat(props: { : [], ); + const [showPromptModal, setShowPromptModal] = useState(false); + return (
@@ -365,7 +488,7 @@ export function Chat(props: { bordered title={Locale.Chat.Actions.CompressedHistory} onClick={() => { - showMemoryPrompt(session); + setShowPromptModal(true); }} />
@@ -380,6 +503,11 @@ export function Chat(props: { />
+ +
@@ -402,7 +530,10 @@ export function Chat(props: { {Locale.Chat.Typing}
)} -
+
inputRef.current?.blur()} + > {!isUser && !(message.preview || message.content.length === 0) && (
@@ -467,7 +598,7 @@ export function Chat(props: { ref={inputRef} className={styles["chat-input"]} placeholder={Locale.Chat.Input(submitKey)} - rows={4} + rows={2} onInput={(e) => onInput(e.currentTarget.value)} value={userInput} onKeyDown={onInputKeyDown} diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 764805d8..24b1f1bf 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -218,6 +218,7 @@ flex: 1; overflow: auto; padding: 20px; + position: relative; } .chat-body-title { diff --git a/app/components/home.tsx b/app/components/home.tsx index f1ce54ad..13db93e2 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -149,11 +149,12 @@ export function Home() { setOpenSettings(true); setShowSideBar(false); }} + shadow />
@@ -165,6 +166,7 @@ export function Home() { createNewSession(); setShowSideBar(false); }} + shadow />
diff --git a/app/components/window.scss b/app/components/window.scss index c1727115..d89c9eb1 100644 --- a/app/components/window.scss +++ b/app/components/window.scss @@ -1,6 +1,7 @@ .window-header { padding: 14px 20px; border-bottom: rgba(0, 0, 0, 0.1) 1px solid; + position: relative; display: flex; justify-content: space-between; @@ -32,4 +33,4 @@ .window-action-button { margin-left: 10px; -} \ No newline at end of file +} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 62be467b..afdcba43 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -138,7 +138,7 @@ const cn = { Topic: "使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”", Summarize: - "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内", + "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 200 字以内", }, ConfirmClearAll: "确认清除所有聊天、设置数据?", }, diff --git a/app/locales/en.ts b/app/locales/en.ts index 98fa7404..87b73b49 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -142,7 +142,7 @@ const en: LocaleType = { Topic: "Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.", Summarize: - "Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.", + "Summarize our discussion briefly in 200 words or less to use as a prompt for future context.", }, ConfirmClearAll: "Confirm to clear all chat and setting data?", }, diff --git a/app/locales/es.ts b/app/locales/es.ts index fca7202d..f195969b 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -142,7 +142,7 @@ const es: LocaleType = { Topic: "Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.", Summarize: - "Resuma nuestra discusión brevemente en 50 caracteres o menos para usarlo como un recordatorio para futuros contextos.", + "Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.", }, ConfirmClearAll: "¿Confirmar para borrar todos los datos de chat y configuración?", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index 27156283..371bca34 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -137,7 +137,7 @@ const tw: LocaleType = { "這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content, Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」", Summarize: - "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內", + "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內", }, ConfirmClearAll: "確認清除所有對話、設定數據?", }, diff --git a/app/store/app.ts b/app/store/app.ts index 7c2b57f1..3e98757c 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -53,6 +53,8 @@ export interface ChatConfig { export type ModelConfig = ChatConfig["modelConfig"]; +export const ROLES: Message["role"][] = ["system", "user", "assistant"]; + const ENABLE_GPT4 = true; export const ALL_MODELS = [ @@ -151,6 +153,7 @@ export interface ChatSession { id: number; topic: string; memoryPrompt: string; + context: Message[]; messages: Message[]; stat: ChatStat; lastUpdate: string; @@ -158,7 +161,7 @@ export interface ChatSession { } const DEFAULT_TOPIC = Locale.Store.DefaultTopic; -export const BOT_HELLO = { +export const BOT_HELLO: Message = { role: "assistant", content: Locale.Store.BotHello, date: "", @@ -171,6 +174,7 @@ function createEmptySession(): ChatSession { id: Date.now(), topic: DEFAULT_TOPIC, memoryPrompt: "", + context: [], messages: [], stat: { tokenCount: 0, @@ -380,16 +384,18 @@ export const useChatStore = create()( const session = get().currentSession(); const config = get().config; const n = session.messages.length; - const recentMessages = session.messages.slice( - Math.max(0, n - config.historyMessageCount), - ); - const memoryPrompt = get().getMemoryPrompt(); + const context = session.context.slice(); - if (session.memoryPrompt) { - recentMessages.unshift(memoryPrompt); + if (session.memoryPrompt && session.memoryPrompt.length > 0) { + const memoryPrompt = get().getMemoryPrompt(); + context.push(memoryPrompt); } + const recentMessages = context.concat( + session.messages.slice(Math.max(0, n - config.historyMessageCount)), + ); + return recentMessages; }, @@ -427,11 +433,13 @@ export const useChatStore = create()( let toBeSummarizedMsgs = session.messages.slice( session.lastSummarizeIndex, ); + const historyMsgLength = countMessages(toBeSummarizedMsgs); - if (historyMsgLength > 4000) { + if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) { + const n = toBeSummarizedMsgs.length; toBeSummarizedMsgs = toBeSummarizedMsgs.slice( - -config.historyMessageCount, + Math.max(0, n - config.historyMessageCount), ); } @@ -494,7 +502,16 @@ export const useChatStore = create()( }), { name: LOCAL_KEY, - version: 1, + version: 1.1, + migrate(persistedState, version) { + const state = persistedState as ChatStore; + + if (version === 1) { + state.sessions.forEach((s) => (s.context = [])); + } + + return state; + }, }, ), ); diff --git a/app/styles/globals.scss b/app/styles/globals.scss index e14ee684..e179dcf3 100644 --- a/app/styles/globals.scss +++ b/app/styles/globals.scss @@ -117,7 +117,7 @@ body { select { border: var(--border-in-light); - padding: 8px 10px; + padding: 10px; border-radius: 10px; appearance: none; cursor: pointer; @@ -188,7 +188,7 @@ input[type="text"] { appearance: none; border-radius: 10px; border: var(--border-in-light); - height: 32px; + height: 36px; box-sizing: border-box; background: var(--white); color: var(--black); @@ -256,3 +256,15 @@ pre { } } } + +.clickable { + cursor: pointer; + + div:not(.no-dark) > svg { + filter: invert(0.5); + } + + &:hover { + filter: brightness(0.9); + } +}