forked from XiaoMo/ChatGPT-Next-Web
feat: add model config to settings
This commit is contained in:
parent
4af8c26d02
commit
2f112ecc54
@ -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%;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
208
app/store.ts
208
app/store.ts
@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user