From 2f112ecc54ca330de42c3996f12ea9b7b406055f Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Tue, 21 Mar 2023 16:20:32 +0000 Subject: [PATCH] feat: add model config to settings --- app/components/home.module.scss | 21 ++- app/components/settings.module.scss | 8 +- app/components/settings.tsx | 235 ++++++++++++++++++++-------- app/locales/cn.ts | 143 +++++++++-------- app/locales/en.ts | 148 ++++++++++-------- app/requests.ts | 12 +- app/store.ts | 208 +++++++++++++++++------- 7 files changed, 517 insertions(+), 258 deletions(-) diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 837c6752..7d011794 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -50,6 +50,8 @@ .window-content { width: var(--window-content-width); height: 100%; + display: flex; + flex-direction: column; } .mobile { @@ -111,7 +113,8 @@ overflow: auto; } -.chat-list {} +.chat-list { +} .chat-item { padding: 10px 14px; @@ -165,12 +168,12 @@ opacity: 0; } -.chat-item:hover>.chat-item-delete { +.chat-item:hover > .chat-item-delete { opacity: 0.5; right: 10px; } -.chat-item:hover>.chat-item-delete:hover { +.chat-item:hover > .chat-item-delete:hover { opacity: 1; } @@ -182,9 +185,11 @@ margin-top: 8px; } -.chat-item-count {} +.chat-item-count { +} -.chat-item-date {} +.chat-item-date { +} .sidebar-tail { display: flex; @@ -232,7 +237,7 @@ animation: slide-in ease 0.3s; } -.chat-message-user>.chat-message-container { +.chat-message-user > .chat-message-container { align-items: flex-end; } @@ -271,7 +276,7 @@ border: var(--border-in-light); } -.chat-message-user>.chat-message-container>.chat-message-item { +.chat-message-user > .chat-message-container > .chat-message-item { background-color: var(--second); } @@ -346,4 +351,4 @@ align-items: center; height: 100%; width: 100%; -} \ No newline at end of file +} diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss index 08be1ff2..ad994f68 100644 --- a/app/components/settings.module.scss +++ b/app/components/settings.module.scss @@ -2,6 +2,7 @@ .settings { padding: 20px; + overflow: auto; } .settings-title { @@ -9,6 +10,11 @@ font-weight: bolder; } +.settings-sub-title { + font-size: 12px; + font-weight: normal; +} + .avatar { cursor: pointer; -} \ No newline at end of file +} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index ebbe8837..4e862f23 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useState } from "react"; import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react"; @@ -11,26 +11,50 @@ import ClearIcon from "../icons/clear.svg"; import { List, ListItem, Popover } from "./ui-lib"; import { IconButton } from "./button"; -import { SubmitKey, useChatStore, Theme } from "../store"; +import { SubmitKey, useChatStore, Theme, ALL_MODELS } from "../store"; import { Avatar } from "./home"; -import Locale, { changeLang, getLang } from '../locales' +import Locale, { changeLang, getLang } from "../locales"; + +function SettingItem(props: { + title: string; + subTitle?: string; + children: JSX.Element; +}) { + return ( + +
+
{props.title}
+ {props.subTitle && ( +
{props.subTitle}
+ )} +
+
{props.children}
+
+ ); +} export function Settings(props: { closeSettings: () => void }) { const [showEmojiPicker, setShowEmojiPicker] = useState(false); - const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [ - state.config, - state.updateConfig, - state.resetConfig, - state.clearAllData, - ]); + const [config, updateConfig, resetConfig, clearAllData] = useChatStore( + (state) => [ + state.config, + state.updateConfig, + state.resetConfig, + state.clearAllData, + ] + ); return ( <>
-
{Locale.Settings.Title}
-
{Locale.Settings.SubTitle}
+
+ {Locale.Settings.Title} +
+
+ {Locale.Settings.SubTitle} +
@@ -61,8 +85,7 @@ export function Settings(props: { closeSettings: () => void }) {
- -
{Locale.Settings.Avatar}
+ setShowEmojiPicker(false)} content={ @@ -84,51 +107,47 @@ export function Settings(props: { closeSettings: () => void }) {
- + + + + + -
{Locale.Settings.SendKey}
-
- +
+ {Locale.Settings.Theme}
+ - -
{Locale.Settings.Theme}
-
- -
-
- - -
{Locale.Settings.TightBorder}
+ void }) { ) } > -
+ - -
{Locale.Settings.Lang.Name}
+
-
+ - -
{Locale.Settings.HistoryCount}
+ void }) { ) } > -
+ - -
- {Locale.Settings.CompressThreshold} -
+ void }) { value={config.compressMessageLengthThreshold} onChange={(e) => updateConfig( - (config) => (config.compressMessageLengthThreshold = e.currentTarget.valueAsNumber) + (config) => + (config.compressMessageLengthThreshold = + e.currentTarget.valueAsNumber) ) } > -
+ +
+ + + + + + + { + updateConfig( + (config) => + (config.modelConfig.temperature = + e.currentTarget.valueAsNumber) + ); + }} + > + + + + updateConfig( + (config) => + (config.modelConfig.max_tokens = + e.currentTarget.valueAsNumber) + ) + } + > + + + { + updateConfig( + (config) => + (config.modelConfig.presence_penalty = + e.currentTarget.valueAsNumber) + ); + }} + > +
diff --git a/app/locales/cn.ts b/app/locales/cn.ts index c7409ca0..dfe12733 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -1,72 +1,93 @@ - const cn = { - ChatItem: { - ChatItemCount: (count: number) => `${count} 条对话`, + ChatItem: { + ChatItemCount: (count: number) => `${count} 条对话`, + }, + Chat: { + SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`, + Actions: { + ChatList: "查看消息列表", + CompressedHistory: "查看压缩后的历史 Prompt", + Export: "导出聊天记录", }, - Chat: { - SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`, - Actions: { - ChatList: '查看消息列表', - CompressedHistory: '查看压缩后的历史 Prompt', - Export: '导出聊天记录', - }, - Typing: '正在输入…', - Input: (submitKey: string) => `输入消息,${submitKey} 发送`, - Send: '发送', + Typing: "正在输入…", + Input: (submitKey: string) => `输入消息,${submitKey} 发送`, + Send: "发送", + }, + Export: { + Title: "导出聊天记录为 Markdown", + Copy: "全部复制", + Download: "下载文件", + }, + Memory: { + Title: "上下文记忆 Prompt", + EmptyContent: "尚未记忆", + Copy: "全部复制", + }, + Home: { + NewChat: "新的聊天", + DeleteChat: "确认删除选中的对话?", + }, + Settings: { + Title: "设置", + SubTitle: "设置选项", + Actions: { + ClearAll: "清除所有数据", + ResetAll: "重置所有选项", + Close: "关闭", }, - Export: { - Title: '导出聊天记录为 Markdown', - Copy: '全部复制', - Download: '下载文件', + Lang: { + Name: "Language", + Options: { + cn: "中文", + en: "English", + }, }, - Memory: { - Title: '上下文记忆 Prompt', - EmptyContent: '尚未记忆', - Copy: '全部复制', + Avatar: "头像", + SendKey: "发送键", + Theme: "主题", + TightBorder: "紧凑边框", + HistoryCount: { + Title: "附带历史消息数", + SubTitle: "每次请求携带的历史消息数", }, - Home: { - NewChat: '新的聊天', - DeleteChat: '确认删除选中的对话?', + CompressThreshold: { + Title: "历史消息长度压缩阈值", + SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", }, - Settings: { - Title: '设置', - SubTitle: '设置选项', - Actions: { - ClearAll: '清除所有数据', - ResetAll: '重置所有选项', - Close: '关闭', - }, - Lang: { - Name: 'Language', - Options: { - cn: '中文', - en: 'English' - } - }, - Avatar: '头像', - SendKey: '发送键', - Theme: '主题', - TightBorder: '紧凑边框', - HistoryCount: '附带历史消息数', - CompressThreshold: '历史消息长度压缩阈值', + Model: "模型 (model)", + Temperature: { + Title: "随机性 (temperature)", + SubTitle: "值越大,回复越随机", }, - Store: { - DefaultTopic: '新的聊天', - BotHello: '有什么可以帮你的吗', - Error: '出错了,稍后重试吧', - Prompt: { - History: (content: string) => '这是 ai 和用户的历史聊天总结作为前情提要:' + content, - Topic: "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”", - Summarize: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内', - }, - ConfirmClearAll: '确认清除所有聊天、设置数据?', + MaxTokens: { + Title: "单次回复限制 (max_tokens)", + SubTitle: "单次交互所用的最大 Token 数", }, - Copy: { - Success: '已写入剪切板', - Failed: '复制失败,请赋予剪切板权限', - } -} + PresencePenlty: { + Title: "话题新鲜度 (presence_penalty)", + SubTitle: "值越大,越有可能扩展到新话题", + }, + }, + Store: { + DefaultTopic: "新的聊天", + BotHello: "有什么可以帮你的吗", + Error: "出错了,稍后重试吧", + Prompt: { + History: (content: string) => + "这是 ai 和用户的历史聊天总结作为前情提要:" + content, + Topic: + "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”", + Summarize: + "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内", + }, + ConfirmClearAll: "确认清除所有聊天、设置数据?", + }, + Copy: { + Success: "已写入剪切板", + Failed: "复制失败,请赋予剪切板权限", + }, +}; export type LocaleType = typeof cn; -export default cn; \ No newline at end of file +export default cn; diff --git a/app/locales/en.ts b/app/locales/en.ts index 55de3afd..dde36a09 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -1,71 +1,97 @@ -import type { LocaleType } from './index' +import type { LocaleType } from "./index"; const en: LocaleType = { - ChatItem: { - ChatItemCount: (count: number) => `${count} messages`, + ChatItem: { + ChatItemCount: (count: number) => `${count} messages`, + }, + Chat: { + SubTitle: (count: number) => `${count} messages with ChatGPT`, + Actions: { + ChatList: "Go To Chat List", + CompressedHistory: "Compressed History Memory Prompt", + Export: "Export All Messages as Markdown", }, - Chat: { - SubTitle: (count: number) => `${count} messages with ChatGPT`, - Actions: { - ChatList: 'Go To Chat List', - CompressedHistory: 'Compressed History Memory Prompt', - Export: 'Export All Messages as Markdown', - }, - Typing: 'Typing…', - Input: (submitKey: string) => `Type something and press ${submitKey} to send`, - Send: 'Send', + Typing: "Typing…", + Input: (submitKey: string) => + `Type something and press ${submitKey} to send`, + Send: "Send", + }, + Export: { + Title: "All Messages", + Copy: "Copy All", + Download: "Download", + }, + Memory: { + Title: "Memory Prompt", + EmptyContent: "Nothing yet.", + Copy: "Copy All", + }, + Home: { + NewChat: "New Chat", + DeleteChat: "Confirm to delete the selected conversation?", + }, + Settings: { + Title: "Settings", + SubTitle: "All Settings", + Actions: { + ClearAll: "Clear All Data", + ResetAll: "Reset All Settings", + Close: "Close", }, - Export: { - Title: 'All Messages', - Copy: 'Copy All', - Download: 'Download', + Lang: { + Name: "语言", + Options: { + cn: "中文", + en: "English", + }, }, - Memory: { - Title: 'Memory Prompt', - EmptyContent: 'Nothing yet.', - Copy: 'Copy All', + Avatar: "Avatar", + SendKey: "Send Key", + Theme: "Theme", + TightBorder: "Tight Border", + HistoryCount: { + Title: "Attached Messages Count", + SubTitle: "Number of sent messages attached per request", }, - Home: { - NewChat: 'New Chat', - DeleteChat: 'Confirm to delete the selected conversation?', + CompressThreshold: { + Title: "History Compression Threshold", + SubTitle: + "Will compress if uncompressed messages length exceeds the value", }, - Settings: { - Title: 'Settings', - SubTitle: 'All Settings', - Actions: { - ClearAll: 'Clear All Data', - ResetAll: 'Reset All Settings', - Close: 'Close', - }, - Lang: { - Name: '语言', - Options: { - cn: '中文', - en: 'English' - } - }, - Avatar: 'Avatar', - SendKey: 'Send Key', - Theme: 'Theme', - TightBorder: 'Tight Border', - HistoryCount: 'History Message Count', - CompressThreshold: 'Message Compression Threshold', + Model: "Model", + Temperature: { + Title: "Temperature", + SubTitle: "A larger value makes the more random output", }, - Store: { - DefaultTopic: 'New Conversation', - BotHello: 'Hello! How can I assist you today?', - Error: 'Something went wrong, please try again later.', - Prompt: { - History: (content: string) => 'This is a summary of the chat history between the AI and the user as a recap: ' + content, - Topic: "Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.", - Summarize: 'Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.', - }, - ConfirmClearAll: 'Confirm to clear all chat and setting data?', + MaxTokens: { + Title: "Max Tokens", + SubTitle: "Maximum length of input tokens and generated tokens", }, - Copy: { - Success: 'Copied to clipboard', - Failed: 'Copy failed, please grant permission to access clipboard', - } -} + PresencePenlty: { + Title: "Presence Penalty", + SubTitle: + "A larger value increases the likelihood to talk about new topics", + }, + }, + Store: { + DefaultTopic: "New Conversation", + BotHello: "Hello! How can I assist you today?", + Error: "Something went wrong, please try again later.", + Prompt: { + History: (content: string) => + "This is a summary of the chat history between the AI and the user as a recap: " + + content, + Topic: + "Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.", + Summarize: + "Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.", + }, + ConfirmClearAll: "Confirm to clear all chat and setting data?", + }, + Copy: { + Success: "Copied to clipboard", + Failed: "Copy failed, please grant permission to access clipboard", + }, +}; -export default en; \ No newline at end of file +export default en; diff --git a/app/requests.ts b/app/requests.ts index 2d1c4609..87f780b3 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -1,7 +1,7 @@ import type { ChatRequest, ChatReponse } from "./api/chat/typing"; -import { Message } from "./store"; +import { filterConfig, isValidModel, Message, ModelConfig } from "./store"; -const TIME_OUT_MS = 30000 +const TIME_OUT_MS = 30000; const makeRequestParam = ( messages: Message[], @@ -44,6 +44,7 @@ export async function requestChatStream( messages: Message[], options?: { filterBot?: boolean; + modelConfig?: ModelConfig; onMessage: (message: string, done: boolean) => void; onError: (error: Error) => void; } @@ -53,6 +54,13 @@ export async function requestChatStream( filterBot: options?.filterBot, }); + // valid and assign model config + if (options?.modelConfig) { + Object.assign(req, filterConfig(options.modelConfig)); + } + + console.log("[Request] ", req); + const controller = new AbortController(); const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS); diff --git a/app/store.ts b/app/store.ts index a0808b12..739c052d 100644 --- a/app/store.ts +++ b/app/store.ts @@ -5,7 +5,7 @@ import { type ChatCompletionResponseMessage } from "openai"; import { requestChatStream, requestWithPrompt } from "./requests"; import { trimTopic } from "./utils"; -import Locale from './locales' +import Locale from "./locales"; export type Message = ChatCompletionResponseMessage & { date: string; @@ -26,7 +26,7 @@ export enum Theme { } export interface ChatConfig { - maxToken?: number + maxToken?: number; historyMessageCount: number; // -1 means all compressMessageLengthThreshold: number; sendBotMessages: boolean; // send bot's message or not @@ -34,6 +34,78 @@ export interface ChatConfig { avatar: string; theme: Theme; tightBorder: boolean; + + modelConfig: { + model: string; + temperature: number; + max_tokens: number; + presence_penalty: number; + }; +} + +export type ModelConfig = ChatConfig["modelConfig"]; + +export const ALL_MODELS = [ + { + name: "gpt-4", + available: false, + }, + { + name: "gpt-4-0314", + available: false, + }, + { + name: "gpt-4-32k", + available: false, + }, + { + name: "gpt-4-32k-0314", + available: false, + }, + { + name: "gpt-3.5-turbo", + available: true, + }, + { + name: "gpt-3.5-turbo-0301", + available: true, + }, +]; + +export function isValidModel(name: string) { + return ALL_MODELS.some((m) => m.name === name && m.available); +} + +export function isValidNumber(x: number, min: number, max: number) { + return typeof x === "number" && x <= max && x >= min; +} + +export function filterConfig(config: ModelConfig): Partial { + const validator: { + [k in keyof ModelConfig]: (x: ModelConfig[keyof ModelConfig]) => boolean; + } = { + model(x) { + return isValidModel(x as string); + }, + max_tokens(x) { + return isValidNumber(x as number, 100, 4000); + }, + presence_penalty(x) { + return isValidNumber(x as number, -2, 2); + }, + temperature(x) { + return isValidNumber(x as number, 0, 1); + }, + }; + + Object.keys(validator).forEach((k) => { + const key = k as keyof ModelConfig; + if (!validator[key](config[key])) { + delete config[key]; + } + }); + + return config; } const DEFAULT_CONFIG: ChatConfig = { @@ -44,6 +116,13 @@ const DEFAULT_CONFIG: ChatConfig = { avatar: "1f603", theme: Theme.Auto as Theme, tightBorder: false, + + modelConfig: { + model: "gpt-3.5-turbo", + temperature: 1, + max_tokens: 2000, + presence_penalty: 0, + }, }; export interface ChatStat { @@ -107,7 +186,7 @@ interface ChatStore { updater: (message?: Message) => void ) => void; getMessagesWithMemory: () => Message[]; - getMemoryPrompt: () => Message, + getMemoryPrompt: () => Message; getConfig: () => ChatConfig; resetConfig: () => void; @@ -193,9 +272,9 @@ export const useChatStore = create()( }, onNewMessage(message) { - get().updateCurrentSession(session => { - session.lastUpdate = new Date().toLocaleString() - }) + get().updateCurrentSession((session) => { + session.lastUpdate = new Date().toLocaleString(); + }); get().updateStat(message); get().summarizeSession(); }, @@ -214,9 +293,9 @@ export const useChatStore = create()( streaming: true, }; - // get recent messages - const recentMessages = get().getMessagesWithMemory() - const sendMessages = recentMessages.concat(userMessage) + // get recent messages + const recentMessages = get().getMessagesWithMemory(); + const sendMessages = recentMessages.concat(userMessage); // save user's and bot's message get().updateCurrentSession((session) => { @@ -224,12 +303,12 @@ export const useChatStore = create()( session.messages.push(botMessage); }); - console.log('[User Input] ', sendMessages) + console.log("[User Input] ", sendMessages); requestChatStream(sendMessages, { onMessage(content, done) { if (done) { botMessage.streaming = false; - get().onNewMessage(botMessage) + get().onNewMessage(botMessage); } else { botMessage.content = content; set(() => ({})); @@ -241,32 +320,35 @@ export const useChatStore = create()( set(() => ({})); }, filterBot: !get().config.sendBotMessages, + modelConfig: get().config.modelConfig, }); }, getMemoryPrompt() { - const session = get().currentSession() + const session = get().currentSession(); return { - role: 'system', + role: "system", content: Locale.Store.Prompt.History(session.memoryPrompt), - date: '' - } as Message + date: "", + } as Message; }, getMessagesWithMemory() { - const session = get().currentSession() - const config = get().config - const n = session.messages.length - const recentMessages = session.messages.slice(n - config.historyMessageCount); + const session = get().currentSession(); + const config = get().config; + const n = session.messages.length; + const recentMessages = session.messages.slice( + n - config.historyMessageCount + ); - const memoryPrompt = get().getMemoryPrompt() + const memoryPrompt = get().getMemoryPrompt(); if (session.memoryPrompt) { - recentMessages.unshift(memoryPrompt) + recentMessages.unshift(memoryPrompt); } - return recentMessages + return recentMessages; }, updateMessage( @@ -286,49 +368,63 @@ export const useChatStore = create()( if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) { // should summarize topic - requestWithPrompt( - session.messages, - Locale.Store.Prompt.Topic - ).then((res) => { - get().updateCurrentSession( - (session) => (session.topic = trimTopic(res)) - ); - }); + requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then( + (res) => { + get().updateCurrentSession( + (session) => (session.topic = trimTopic(res)) + ); + } + ); } - const config = get().config - let toBeSummarizedMsgs = session.messages.slice(session.lastSummarizeIndex) - const historyMsgLength = toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0) + const config = get().config; + let toBeSummarizedMsgs = session.messages.slice( + session.lastSummarizeIndex + ); + const historyMsgLength = toBeSummarizedMsgs.reduce( + (pre, cur) => pre + cur.content.length, + 0 + ); if (historyMsgLength > 4000) { - toBeSummarizedMsgs = toBeSummarizedMsgs.slice(-config.historyMessageCount) + toBeSummarizedMsgs = toBeSummarizedMsgs.slice( + -config.historyMessageCount + ); } // add memory prompt - toBeSummarizedMsgs.unshift(get().getMemoryPrompt()) + toBeSummarizedMsgs.unshift(get().getMemoryPrompt()); - const lastSummarizeIndex = session.messages.length + const lastSummarizeIndex = session.messages.length; - console.log('[Chat History] ', toBeSummarizedMsgs, historyMsgLength, config.compressMessageLengthThreshold) + console.log( + "[Chat History] ", + toBeSummarizedMsgs, + historyMsgLength, + config.compressMessageLengthThreshold + ); if (historyMsgLength > config.compressMessageLengthThreshold) { - requestChatStream(toBeSummarizedMsgs.concat({ - role: 'system', - content: Locale.Store.Prompt.Summarize, - date: '' - }), { - filterBot: false, - onMessage(message, done) { - session.memoryPrompt = message - if (done) { - console.log('[Memory] ', session.memoryPrompt) - session.lastSummarizeIndex = lastSummarizeIndex - } - }, - onError(error) { - console.error('[Summarize] ', error) - }, - }) + requestChatStream( + toBeSummarizedMsgs.concat({ + role: "system", + content: Locale.Store.Prompt.Summarize, + date: "", + }), + { + filterBot: false, + onMessage(message, done) { + session.memoryPrompt = message; + if (done) { + console.log("[Memory] ", session.memoryPrompt); + session.lastSummarizeIndex = lastSummarizeIndex; + } + }, + onError(error) { + console.error("[Summarize] ", error); + }, + } + ); } }, @@ -348,8 +444,8 @@ export const useChatStore = create()( clearAllData() { if (confirm(Locale.Store.ConfirmClearAll)) { - localStorage.clear() - location.reload() + localStorage.clear(); + location.reload(); } }, }),