feat: add model config to settings

This commit is contained in:
Yifei Zhang 2023-03-21 16:20:32 +00:00
parent 4af8c26d02
commit 2f112ecc54
7 changed files with 517 additions and 258 deletions

View File

@ -50,6 +50,8 @@
.window-content { .window-content {
width: var(--window-content-width); width: var(--window-content-width);
height: 100%; height: 100%;
display: flex;
flex-direction: column;
} }
.mobile { .mobile {
@ -111,7 +113,8 @@
overflow: auto; overflow: auto;
} }
.chat-list {} .chat-list {
}
.chat-item { .chat-item {
padding: 10px 14px; padding: 10px 14px;
@ -165,12 +168,12 @@
opacity: 0; opacity: 0;
} }
.chat-item:hover>.chat-item-delete { .chat-item:hover > .chat-item-delete {
opacity: 0.5; opacity: 0.5;
right: 10px; right: 10px;
} }
.chat-item:hover>.chat-item-delete:hover { .chat-item:hover > .chat-item-delete:hover {
opacity: 1; opacity: 1;
} }
@ -182,9 +185,11 @@
margin-top: 8px; margin-top: 8px;
} }
.chat-item-count {} .chat-item-count {
}
.chat-item-date {} .chat-item-date {
}
.sidebar-tail { .sidebar-tail {
display: flex; display: flex;
@ -232,7 +237,7 @@
animation: slide-in ease 0.3s; animation: slide-in ease 0.3s;
} }
.chat-message-user>.chat-message-container { .chat-message-user > .chat-message-container {
align-items: flex-end; align-items: flex-end;
} }
@ -271,7 +276,7 @@
border: var(--border-in-light); 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); background-color: var(--second);
} }
@ -346,4 +351,4 @@
align-items: center; align-items: center;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }

View File

@ -2,6 +2,7 @@
.settings { .settings {
padding: 20px; padding: 20px;
overflow: auto;
} }
.settings-title { .settings-title {
@ -9,6 +10,11 @@
font-weight: bolder; font-weight: bolder;
} }
.settings-sub-title {
font-size: 12px;
font-weight: normal;
}
.avatar { .avatar {
cursor: pointer; cursor: pointer;
} }

View File

@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from "react"; import { useState } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-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 { List, ListItem, Popover } from "./ui-lib";
import { IconButton } from "./button"; import { IconButton } from "./button";
import { SubmitKey, useChatStore, Theme } from "../store"; import { SubmitKey, useChatStore, Theme, ALL_MODELS } from "../store";
import { Avatar } from "./home"; 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 (
<ListItem>
<div className={styles["settings-title"]}>
<div>{props.title}</div>
{props.subTitle && (
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
)}
</div>
<div>{props.children}</div>
</ListItem>
);
}
export function Settings(props: { closeSettings: () => void }) { export function Settings(props: { closeSettings: () => void }) {
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [ const [config, updateConfig, resetConfig, clearAllData] = useChatStore(
state.config, (state) => [
state.updateConfig, state.config,
state.resetConfig, state.updateConfig,
state.clearAllData, state.resetConfig,
]); state.clearAllData,
]
);
return ( return (
<> <>
<div className={styles["window-header"]}> <div className={styles["window-header"]}>
<div className={styles["window-header-title"]}> <div className={styles["window-header-title"]}>
<div className={styles["window-header-main-title"]}>{Locale.Settings.Title}</div> <div className={styles["window-header-main-title"]}>
<div className={styles["window-header-sub-title"]}>{Locale.Settings.SubTitle}</div> {Locale.Settings.Title}
</div>
<div className={styles["window-header-sub-title"]}>
{Locale.Settings.SubTitle}
</div>
</div> </div>
<div className={styles["window-actions"]}> <div className={styles["window-actions"]}>
<div className={styles["window-action-button"]}> <div className={styles["window-action-button"]}>
@ -61,8 +85,7 @@ export function Settings(props: { closeSettings: () => void }) {
</div> </div>
<div className={styles["settings"]}> <div className={styles["settings"]}>
<List> <List>
<ListItem> <SettingItem title={Locale.Settings.Avatar}>
<div className={styles["settings-title"]}>{Locale.Settings.Avatar}</div>
<Popover <Popover
onClose={() => setShowEmojiPicker(false)} onClose={() => setShowEmojiPicker(false)}
content={ content={
@ -84,51 +107,47 @@ export function Settings(props: { closeSettings: () => void }) {
<Avatar role="user" /> <Avatar role="user" />
</div> </div>
</Popover> </Popover>
</ListItem> </SettingItem>
<SettingItem title={Locale.Settings.SendKey}>
<select
value={config.submitKey}
onChange={(e) => {
updateConfig(
(config) =>
(config.submitKey = e.target.value as any as SubmitKey)
);
}}
>
{Object.values(SubmitKey).map((v) => (
<option value={v} key={v}>
{v}
</option>
))}
</select>
</SettingItem>
<ListItem> <ListItem>
<div className={styles["settings-title"]}>{Locale.Settings.SendKey}</div> <div className={styles["settings-title"]}>
<div className=""> {Locale.Settings.Theme}
<select
value={config.submitKey}
onChange={(e) => {
updateConfig(
(config) =>
(config.submitKey = e.target.value as any as SubmitKey)
);
}}
>
{Object.values(SubmitKey).map((v) => (
<option value={v} key={v}>
{v}
</option>
))}
</select>
</div> </div>
<select
value={config.theme}
onChange={(e) => {
updateConfig(
(config) => (config.theme = e.target.value as any as Theme)
);
}}
>
{Object.values(Theme).map((v) => (
<option value={v} key={v}>
{v}
</option>
))}
</select>
</ListItem> </ListItem>
<ListItem> <SettingItem title={Locale.Settings.TightBorder}>
<div className={styles["settings-title"]}>{Locale.Settings.Theme}</div>
<div className="">
<select
value={config.theme}
onChange={(e) => {
updateConfig(
(config) => (config.theme = e.target.value as any as Theme)
);
}}
>
{Object.values(Theme).map((v) => (
<option value={v} key={v}>
{v}
</option>
))}
</select>
</div>
</ListItem>
<ListItem>
<div className={styles["settings-title"]}>{Locale.Settings.TightBorder}</div>
<input <input
type="checkbox" type="checkbox"
checked={config.tightBorder} checked={config.tightBorder}
@ -138,31 +157,32 @@ export function Settings(props: { closeSettings: () => void }) {
) )
} }
></input> ></input>
</ListItem> </SettingItem>
<ListItem> <SettingItem title={Locale.Settings.Lang.Name}>
<div className={styles["settings-title"]}>{Locale.Settings.Lang.Name}</div>
<div className=""> <div className="">
<select <select
value={getLang()} value={getLang()}
onChange={(e) => { onChange={(e) => {
changeLang(e.target.value as any) changeLang(e.target.value as any);
}} }}
> >
<option value='en' key='en'> <option value="en" key="en">
{Locale.Settings.Lang.Options.en} {Locale.Settings.Lang.Options.en}
</option> </option>
<option value='cn' key='cn'> <option value="cn" key="cn">
{Locale.Settings.Lang.Options.cn} {Locale.Settings.Lang.Options.cn}
</option> </option>
</select> </select>
</div> </div>
</ListItem> </SettingItem>
</List> </List>
<List> <List>
<ListItem> <SettingItem
<div className={styles["settings-title"]}>{Locale.Settings.HistoryCount}</div> title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<input <input
type="range" type="range"
title={config.historyMessageCount.toString()} title={config.historyMessageCount.toString()}
@ -177,12 +197,12 @@ export function Settings(props: { closeSettings: () => void }) {
) )
} }
></input> ></input>
</ListItem> </SettingItem>
<ListItem> <SettingItem
<div className={styles["settings-title"]}> title={Locale.Settings.CompressThreshold.Title}
{Locale.Settings.CompressThreshold} subTitle={Locale.Settings.CompressThreshold.SubTitle}
</div> >
<input <input
type="number" type="number"
min={500} min={500}
@ -190,11 +210,88 @@ export function Settings(props: { closeSettings: () => void }) {
value={config.compressMessageLengthThreshold} value={config.compressMessageLengthThreshold}
onChange={(e) => onChange={(e) =>
updateConfig( updateConfig(
(config) => (config.compressMessageLengthThreshold = e.currentTarget.valueAsNumber) (config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber)
) )
} }
></input> ></input>
</ListItem> </SettingItem>
</List>
<List>
<SettingItem title={Locale.Settings.Model}>
<select
value={config.modelConfig.model}
onChange={(e) => {
updateConfig(
(config) => (config.modelConfig.model = e.currentTarget.value)
);
}}
>
{ALL_MODELS.map((v) => (
<option value={v.name} key={v.name} disabled={!v.available}>
{v.name}
</option>
))}
</select>
</SettingItem>
<SettingItem
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<input
type="range"
value={config.modelConfig.temperature.toFixed(1)}
min="0"
max="1"
step="0.1"
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.temperature =
e.currentTarget.valueAsNumber)
);
}}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={4000}
value={config.modelConfig.max_tokens}
onChange={(e) =>
updateConfig(
(config) =>
(config.modelConfig.max_tokens =
e.currentTarget.valueAsNumber)
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
<input
type="range"
value={config.modelConfig.presence_penalty.toFixed(1)}
min="-2"
max="2"
step="0.5"
onChange={(e) => {
updateConfig(
(config) =>
(config.modelConfig.presence_penalty =
e.currentTarget.valueAsNumber)
);
}}
></input>
</SettingItem>
</List> </List>
</div> </div>
</> </>

View File

@ -1,72 +1,93 @@
const cn = { const cn = {
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`, ChatItemCount: (count: number) => `${count} 条对话`,
},
Chat: {
SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`,
Actions: {
ChatList: "查看消息列表",
CompressedHistory: "查看压缩后的历史 Prompt",
Export: "导出聊天记录",
}, },
Chat: { Typing: "正在输入…",
SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`, Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
Actions: { Send: "发送",
ChatList: '查看消息列表', },
CompressedHistory: '查看压缩后的历史 Prompt', Export: {
Export: '导出聊天记录', Title: "导出聊天记录为 Markdown",
}, Copy: "全部复制",
Typing: '正在输入…', Download: "下载文件",
Input: (submitKey: string) => `输入消息,${submitKey} 发送`, },
Send: '发送', Memory: {
Title: "上下文记忆 Prompt",
EmptyContent: "尚未记忆",
Copy: "全部复制",
},
Home: {
NewChat: "新的聊天",
DeleteChat: "确认删除选中的对话?",
},
Settings: {
Title: "设置",
SubTitle: "设置选项",
Actions: {
ClearAll: "清除所有数据",
ResetAll: "重置所有选项",
Close: "关闭",
}, },
Export: { Lang: {
Title: '导出聊天记录为 Markdown', Name: "Language",
Copy: '全部复制', Options: {
Download: '下载文件', cn: "中文",
en: "English",
},
}, },
Memory: { Avatar: "头像",
Title: '上下文记忆 Prompt', SendKey: "发送键",
EmptyContent: '尚未记忆', Theme: "主题",
Copy: '全部复制', TightBorder: "紧凑边框",
HistoryCount: {
Title: "附带历史消息数",
SubTitle: "每次请求携带的历史消息数",
}, },
Home: { CompressThreshold: {
NewChat: '新的聊天', Title: "历史消息长度压缩阈值",
DeleteChat: '确认删除选中的对话?', SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
}, },
Settings: { Model: "模型 (model)",
Title: '设置', Temperature: {
SubTitle: '设置选项', Title: "随机性 (temperature)",
Actions: { SubTitle: "值越大,回复越随机",
ClearAll: '清除所有数据',
ResetAll: '重置所有选项',
Close: '关闭',
},
Lang: {
Name: 'Language',
Options: {
cn: '中文',
en: 'English'
}
},
Avatar: '头像',
SendKey: '发送键',
Theme: '主题',
TightBorder: '紧凑边框',
HistoryCount: '附带历史消息数',
CompressThreshold: '历史消息长度压缩阈值',
}, },
Store: { MaxTokens: {
DefaultTopic: '新的聊天', Title: "单次回复限制 (max_tokens)",
BotHello: '有什么可以帮你的吗', SubTitle: "单次交互所用的最大 Token 数",
Error: '出错了,稍后重试吧',
Prompt: {
History: (content: string) => '这是 ai 和用户的历史聊天总结作为前情提要:' + content,
Topic: "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
Summarize: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 50 字以内',
},
ConfirmClearAll: '确认清除所有聊天、设置数据?',
}, },
Copy: { PresencePenlty: {
Success: '已写入剪切板', Title: "话题新鲜度 (presence_penalty)",
Failed: '复制失败,请赋予剪切板权限', 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 type LocaleType = typeof cn;
export default cn; export default cn;

View File

@ -1,71 +1,97 @@
import type { LocaleType } from './index' import type { LocaleType } from "./index";
const en: LocaleType = { const en: LocaleType = {
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} messages`, 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: { Typing: "Typing…",
SubTitle: (count: number) => `${count} messages with ChatGPT`, Input: (submitKey: string) =>
Actions: { `Type something and press ${submitKey} to send`,
ChatList: 'Go To Chat List', Send: "Send",
CompressedHistory: 'Compressed History Memory Prompt', },
Export: 'Export All Messages as Markdown', Export: {
}, Title: "All Messages",
Typing: 'Typing…', Copy: "Copy All",
Input: (submitKey: string) => `Type something and press ${submitKey} to send`, Download: "Download",
Send: 'Send', },
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: { Lang: {
Title: 'All Messages', Name: "语言",
Copy: 'Copy All', Options: {
Download: 'Download', cn: "中文",
en: "English",
},
}, },
Memory: { Avatar: "Avatar",
Title: 'Memory Prompt', SendKey: "Send Key",
EmptyContent: 'Nothing yet.', Theme: "Theme",
Copy: 'Copy All', TightBorder: "Tight Border",
HistoryCount: {
Title: "Attached Messages Count",
SubTitle: "Number of sent messages attached per request",
}, },
Home: { CompressThreshold: {
NewChat: 'New Chat', Title: "History Compression Threshold",
DeleteChat: 'Confirm to delete the selected conversation?', SubTitle:
"Will compress if uncompressed messages length exceeds the value",
}, },
Settings: { Model: "Model",
Title: 'Settings', Temperature: {
SubTitle: 'All Settings', Title: "Temperature",
Actions: { SubTitle: "A larger value makes the more random output",
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',
}, },
Store: { MaxTokens: {
DefaultTopic: 'New Conversation', Title: "Max Tokens",
BotHello: 'Hello! How can I assist you today?', SubTitle: "Maximum length of input tokens and generated tokens",
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: { PresencePenlty: {
Success: 'Copied to clipboard', Title: "Presence Penalty",
Failed: 'Copy failed, please grant permission to access clipboard', 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; export default en;

View File

@ -1,7 +1,7 @@
import type { ChatRequest, ChatReponse } from "./api/chat/typing"; 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 = ( const makeRequestParam = (
messages: Message[], messages: Message[],
@ -44,6 +44,7 @@ export async function requestChatStream(
messages: Message[], messages: Message[],
options?: { options?: {
filterBot?: boolean; filterBot?: boolean;
modelConfig?: ModelConfig;
onMessage: (message: string, done: boolean) => void; onMessage: (message: string, done: boolean) => void;
onError: (error: Error) => void; onError: (error: Error) => void;
} }
@ -53,6 +54,13 @@ export async function requestChatStream(
filterBot: options?.filterBot, 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 controller = new AbortController();
const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS); const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS);

View File

@ -5,7 +5,7 @@ import { type ChatCompletionResponseMessage } from "openai";
import { requestChatStream, requestWithPrompt } from "./requests"; import { requestChatStream, requestWithPrompt } from "./requests";
import { trimTopic } from "./utils"; import { trimTopic } from "./utils";
import Locale from './locales' import Locale from "./locales";
export type Message = ChatCompletionResponseMessage & { export type Message = ChatCompletionResponseMessage & {
date: string; date: string;
@ -26,7 +26,7 @@ export enum Theme {
} }
export interface ChatConfig { export interface ChatConfig {
maxToken?: number maxToken?: number;
historyMessageCount: number; // -1 means all historyMessageCount: number; // -1 means all
compressMessageLengthThreshold: number; compressMessageLengthThreshold: number;
sendBotMessages: boolean; // send bot's message or not sendBotMessages: boolean; // send bot's message or not
@ -34,6 +34,78 @@ export interface ChatConfig {
avatar: string; avatar: string;
theme: Theme; theme: Theme;
tightBorder: boolean; 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<ModelConfig> {
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 = { const DEFAULT_CONFIG: ChatConfig = {
@ -44,6 +116,13 @@ const DEFAULT_CONFIG: ChatConfig = {
avatar: "1f603", avatar: "1f603",
theme: Theme.Auto as Theme, theme: Theme.Auto as Theme,
tightBorder: false, tightBorder: false,
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
},
}; };
export interface ChatStat { export interface ChatStat {
@ -107,7 +186,7 @@ interface ChatStore {
updater: (message?: Message) => void updater: (message?: Message) => void
) => void; ) => void;
getMessagesWithMemory: () => Message[]; getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message, getMemoryPrompt: () => Message;
getConfig: () => ChatConfig; getConfig: () => ChatConfig;
resetConfig: () => void; resetConfig: () => void;
@ -193,9 +272,9 @@ export const useChatStore = create<ChatStore>()(
}, },
onNewMessage(message) { onNewMessage(message) {
get().updateCurrentSession(session => { get().updateCurrentSession((session) => {
session.lastUpdate = new Date().toLocaleString() session.lastUpdate = new Date().toLocaleString();
}) });
get().updateStat(message); get().updateStat(message);
get().summarizeSession(); get().summarizeSession();
}, },
@ -214,9 +293,9 @@ export const useChatStore = create<ChatStore>()(
streaming: true, streaming: true,
}; };
// get recent messages // get recent messages
const recentMessages = get().getMessagesWithMemory() const recentMessages = get().getMessagesWithMemory();
const sendMessages = recentMessages.concat(userMessage) const sendMessages = recentMessages.concat(userMessage);
// save user's and bot's message // save user's and bot's message
get().updateCurrentSession((session) => { get().updateCurrentSession((session) => {
@ -224,12 +303,12 @@ export const useChatStore = create<ChatStore>()(
session.messages.push(botMessage); session.messages.push(botMessage);
}); });
console.log('[User Input] ', sendMessages) console.log("[User Input] ", sendMessages);
requestChatStream(sendMessages, { requestChatStream(sendMessages, {
onMessage(content, done) { onMessage(content, done) {
if (done) { if (done) {
botMessage.streaming = false; botMessage.streaming = false;
get().onNewMessage(botMessage) get().onNewMessage(botMessage);
} else { } else {
botMessage.content = content; botMessage.content = content;
set(() => ({})); set(() => ({}));
@ -241,32 +320,35 @@ export const useChatStore = create<ChatStore>()(
set(() => ({})); set(() => ({}));
}, },
filterBot: !get().config.sendBotMessages, filterBot: !get().config.sendBotMessages,
modelConfig: get().config.modelConfig,
}); });
}, },
getMemoryPrompt() { getMemoryPrompt() {
const session = get().currentSession() const session = get().currentSession();
return { return {
role: 'system', role: "system",
content: Locale.Store.Prompt.History(session.memoryPrompt), content: Locale.Store.Prompt.History(session.memoryPrompt),
date: '' date: "",
} as Message } as Message;
}, },
getMessagesWithMemory() { getMessagesWithMemory() {
const session = get().currentSession() const session = get().currentSession();
const config = get().config const config = get().config;
const n = session.messages.length const n = session.messages.length;
const recentMessages = session.messages.slice(n - config.historyMessageCount); const recentMessages = session.messages.slice(
n - config.historyMessageCount
);
const memoryPrompt = get().getMemoryPrompt() const memoryPrompt = get().getMemoryPrompt();
if (session.memoryPrompt) { if (session.memoryPrompt) {
recentMessages.unshift(memoryPrompt) recentMessages.unshift(memoryPrompt);
} }
return recentMessages return recentMessages;
}, },
updateMessage( updateMessage(
@ -286,49 +368,63 @@ export const useChatStore = create<ChatStore>()(
if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) { if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) {
// should summarize topic // should summarize topic
requestWithPrompt( requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
session.messages, (res) => {
Locale.Store.Prompt.Topic get().updateCurrentSession(
).then((res) => { (session) => (session.topic = trimTopic(res))
get().updateCurrentSession( );
(session) => (session.topic = trimTopic(res)) }
); );
});
} }
const config = get().config const config = get().config;
let toBeSummarizedMsgs = session.messages.slice(session.lastSummarizeIndex) let toBeSummarizedMsgs = session.messages.slice(
const historyMsgLength = toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0) session.lastSummarizeIndex
);
const historyMsgLength = toBeSummarizedMsgs.reduce(
(pre, cur) => pre + cur.content.length,
0
);
if (historyMsgLength > 4000) { if (historyMsgLength > 4000) {
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(-config.historyMessageCount) toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
-config.historyMessageCount
);
} }
// add memory prompt // 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) { if (historyMsgLength > config.compressMessageLengthThreshold) {
requestChatStream(toBeSummarizedMsgs.concat({ requestChatStream(
role: 'system', toBeSummarizedMsgs.concat({
content: Locale.Store.Prompt.Summarize, role: "system",
date: '' content: Locale.Store.Prompt.Summarize,
}), { date: "",
filterBot: false, }),
onMessage(message, done) { {
session.memoryPrompt = message filterBot: false,
if (done) { onMessage(message, done) {
console.log('[Memory] ', session.memoryPrompt) session.memoryPrompt = message;
session.lastSummarizeIndex = lastSummarizeIndex if (done) {
} console.log("[Memory] ", session.memoryPrompt);
}, session.lastSummarizeIndex = lastSummarizeIndex;
onError(error) { }
console.error('[Summarize] ', error) },
}, onError(error) {
}) console.error("[Summarize] ", error);
},
}
);
} }
}, },
@ -348,8 +444,8 @@ export const useChatStore = create<ChatStore>()(
clearAllData() { clearAllData() {
if (confirm(Locale.Store.ConfirmClearAll)) { if (confirm(Locale.Store.ConfirmClearAll)) {
localStorage.clear() localStorage.clear();
location.reload() location.reload();
} }
}, },
}), }),