feat: add session config modal

This commit is contained in:
Yidadaa 2023-04-23 01:27:15 +08:00
parent 2e3aa940fb
commit 7345639af3
15 changed files with 489 additions and 398 deletions

View File

@ -53,6 +53,20 @@
} }
} }
.section-title {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.section-title-action {
display: flex;
align-items: center;
}
}
.context-prompt { .context-prompt {
.context-prompt-row { .context-prompt-row {
display: flex; display: flex;
@ -81,25 +95,13 @@
} }
.memory-prompt { .memory-prompt {
margin-top: 20px; margin: 20px 0;
.memory-prompt-title {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.memory-prompt-action {
display: flex;
align-items: center;
}
}
.memory-prompt-content { .memory-prompt-content {
background-color: var(--gray); background-color: var(--white);
border-radius: 6px; color: var(--black);
border: var(--border-in-light);
border-radius: 10px;
padding: 10px; padding: 10px;
font-size: 12px; font-size: 12px;
user-select: text; user-select: text;

View File

@ -1,4 +1,4 @@
import { useDebounce, useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react"; import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
@ -9,8 +9,6 @@ import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg"; import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
import AddIcon from "../icons/add.svg"; import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg"; import DeleteIcon from "../icons/delete.svg";
import MaxIcon from "../icons/max.svg"; import MaxIcon from "../icons/max.svg";
@ -33,12 +31,13 @@ import {
Theme, Theme,
ModelType, ModelType,
useAppConfig, useAppConfig,
ModelConfig,
DEFAULT_TOPIC,
} from "../store"; } from "../store";
import { import {
copyToClipboard, copyToClipboard,
downloadAs, downloadAs,
getEmojiUrl,
selectOrCopy, selectOrCopy,
autoGrowTextArea, autoGrowTextArea,
useMobileScreen, useMobileScreen,
@ -54,10 +53,11 @@ import { IconButton } from "./button";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss"; import chatStyle from "./chat.module.scss";
import { Input, Modal, showModal } from "./ui-lib"; import { Input, List, ListItem, Modal, Popover, showModal } from "./ui-lib";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Path } from "../constant"; import { Path } from "../constant";
import { ModelConfigList } from "./model-config";
import { AvatarPicker } from "./emoji";
const Markdown = dynamic( const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown), async () => memo((await import("./markdown")).Markdown),
{ {
@ -65,32 +65,10 @@ const Markdown = dynamic(
}, },
); );
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { const Avatar = dynamic(async () => (await import("./emoji")).Avatar, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
}); });
export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
const config = useAppConfig();
if (props.role !== "user") {
return (
<div className="no-dark">
{props.model?.startsWith("gpt-4") ? (
<BlackBotIcon className={styles["user-avtar"]} />
) : (
<BotIcon className={styles["user-avtar"]} />
)}
</div>
);
}
return (
<div className={styles["user-avtar"]}>
<Emoji unified={config.avatar} size={18} getEmojiUrl={getEmojiUrl} />
</div>
);
}
function exportMessages(messages: Message[], topic: string) { function exportMessages(messages: Message[], topic: string) {
const mdText = const mdText =
`# ${topic}\n\n` + `# ${topic}\n\n` +
@ -129,15 +107,13 @@ function exportMessages(messages: Message[], topic: string) {
}); });
} }
function PromptToast(props: { function ContextPrompts() {
showToast?: boolean;
showModal?: boolean;
setShowModal: (_: boolean) => void;
}) {
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const context = session.context; const context = session.context;
const [showPicker, setShowPicker] = useState(false);
const addContextPrompt = (prompt: Message) => { const addContextPrompt = (prompt: Message) => {
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
session.context.push(prompt); session.context.push(prompt);
@ -156,6 +132,165 @@ function PromptToast(props: {
}); });
}; };
return (
<>
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
{context.map((c, i) => (
<div className={chatStyle["context-prompt-row"]} key={i}>
<select
value={c.role}
className={chatStyle["context-role"]}
onChange={(e) =>
updateContextPrompt(i, {
...c,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
<Input
value={c.content}
type="text"
className={chatStyle["context-content"]}
rows={1}
onInput={(e) =>
updateContextPrompt(i, {
...c,
content: e.currentTarget.value as any,
})
}
/>
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => removeContextPrompt(i)}
bordered
/>
</div>
))}
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "system",
content: "",
date: "",
})
}
/>
</div>
</div>
<List>
<ListItem title={"角色头像"}>
<Popover
content={
<AvatarPicker
onEmojiClick={(emoji) =>
chatStore.updateCurrentSession(
(session) => (session.avatar = emoji),
)
}
></AvatarPicker>
}
open={showPicker}
onClose={() => setShowPicker(false)}
>
<div onClick={() => setShowPicker(true)}>
{session.avatar ? (
<Avatar avatar={session.avatar} />
) : (
<Avatar model={session.modelConfig.model} />
)}
</div>
</Popover>
</ListItem>
<ListItem title={"对话标题"}>
<input
type="text"
value={session.topic}
onInput={(e) =>
chatStore.updateCurrentSession(
(session) => (session.topic = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of
${session.messages.length})`}
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
></ListItem>
</List>
</>
);
}
export function SessionConfigModel(props: { onClose: () => void }) {
const chatStore = useChatStore();
const config = useAppConfig();
const session = chatStore.currentSession();
const context = session.context;
const updateConfig = (updater: (config: ModelConfig) => void) => {
const config = { ...session.modelConfig };
updater(config);
chatStore.updateCurrentSession((session) => (session.modelConfig = config));
};
return (
<div className="modal-mask">
<Modal
title={Locale.Context.Edit}
onClose={() => props.onClose()}
actions={[
<IconButton
key="reset"
icon={<CopyIcon />}
bordered
text="重置预设"
onClick={() =>
confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text="保存预设"
onClick={() => copyToClipboard(session.memoryPrompt)}
/>,
]}
>
<ContextPrompts />
<ModelConfigList
modelConfig={session.modelConfig}
updateConfig={updateConfig}
/>
</Modal>
</div>
);
}
function PromptToast(props: {
showToast?: boolean;
showModal?: boolean;
setShowModal: (_: boolean) => void;
}) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const context = session.context;
return ( return (
<div className={chatStyle["prompt-toast"]} key="prompt-toast"> <div className={chatStyle["prompt-toast"]} key="prompt-toast">
{props.showToast && ( {props.showToast && (
@ -171,115 +306,7 @@ function PromptToast(props: {
</div> </div>
)} )}
{props.showModal && ( {props.showModal && (
<div className="modal-mask"> <SessionConfigModel onClose={() => props.setShowModal(false)} />
<Modal
title={Locale.Context.Edit}
onClose={() => props.setShowModal(false)}
actions={[
<IconButton
key="reset"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Reset}
onClick={() =>
confirm(Locale.Memory.ResetConfirm) &&
chatStore.resetSession()
}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Copy}
onClick={() => copyToClipboard(session.memoryPrompt)}
/>,
]}
>
<>
<div className={chatStyle["context-prompt"]}>
{context.map((c, i) => (
<div className={chatStyle["context-prompt-row"]} key={i}>
<select
value={c.role}
className={chatStyle["context-role"]}
onChange={(e) =>
updateContextPrompt(i, {
...c,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
<Input
value={c.content}
type="text"
className={chatStyle["context-content"]}
rows={1}
onInput={(e) =>
updateContextPrompt(i, {
...c,
content: e.currentTarget.value as any,
})
}
/>
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => removeContextPrompt(i)}
bordered
/>
</div>
))}
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "system",
content: "",
date: "",
})
}
/>
</div>
</div>
<div className={chatStyle["memory-prompt"]}>
<div className={chatStyle["memory-prompt-title"]}>
<span>
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
{session.messages.length})
</span>
<label className={chatStyle["memory-prompt-action"]}>
{Locale.Memory.Send}
<input
type="checkbox"
checked={session.sendMemory}
onChange={() =>
chatStore.updateCurrentSession(
(session) =>
(session.sendMemory = !session.sendMemory),
)
}
></input>
</label>
</div>
<div className={chatStyle["memory-prompt-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
</div>
</div>
</>
</Modal>
</div>
)} )}
</div> </div>
); );
@ -654,7 +681,7 @@ export function Chat() {
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`} className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClickCapture={renameSession} onClickCapture={renameSession}
> >
{session.topic} {!session.topic ? DEFAULT_TOPIC : session.topic}
</div> </div>
<div className={styles["window-header-sub-title"]}> <div className={styles["window-header-sub-title"]}>
{Locale.Chat.SubTitle(session.messages.length)} {Locale.Chat.SubTitle(session.messages.length)}
@ -739,7 +766,13 @@ export function Chat() {
> >
<div className={styles["chat-message-container"]}> <div className={styles["chat-message-container"]}>
<div className={styles["chat-message-avatar"]}> <div className={styles["chat-message-avatar"]}>
<Avatar role={message.role} model={message.model} /> {message.role === "user" ? (
<Avatar avatar={config.avatar} />
) : session.avatar ? (
<Avatar avatar={session.avatar} />
) : (
<Avatar model={message.model ?? "gpt-3.5-turbo"} />
)}
</div> </div>
{showTyping && ( {showTyping && (
<div className={styles["chat-message-status"]}> <div className={styles["chat-message-status"]}>

59
app/components/emoji.tsx Normal file
View File

@ -0,0 +1,59 @@
import EmojiPicker, {
Emoji,
EmojiStyle,
Theme as EmojiTheme,
} from "emoji-picker-react";
import { ModelType } from "../store";
import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}
export function AvatarPicker(props: {
onEmojiClick: (emojiId: string) => void;
}) {
return (
<EmojiPicker
lazyLoadEmojis
theme={EmojiTheme.AUTO}
getEmojiUrl={getEmojiUrl}
onEmojiClick={(e) => {
props.onEmojiClick(e.unified);
}}
/>
);
}
export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) {
return (
<div className="no-dark">
{props.model?.startsWith("gpt-4") ? (
<BlackBotIcon className="user-avtar" />
) : (
<BotIcon className="user-avtar" />
)}
</div>
);
}
return (
<div className="user-avtar">
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
</div>
);
}
export function EmojiAvatar(props: { avatar: string; size?: number }) {
return (
<Emoji
unified={props.avatar}
size={props.size ?? 18}
getEmojiUrl={getEmojiUrl}
/>
);
}

View File

@ -368,17 +368,6 @@
margin-top: 5px; margin-top: 5px;
} }
.user-avtar {
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
border-radius: 10px;
}
.chat-message-item { .chat-message-item {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;

View File

@ -0,0 +1,141 @@
import styles from "./settings.module.scss";
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales";
import { InputRange } from "./input-range";
import { List, ListItem } from "./ui-lib";
export function ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
return (
<List>
<ListItem title={Locale.Settings.Model}>
<select
value={props.modelConfig.model}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.model = ModalConfigValidator.model(
e.currentTarget.value,
)),
);
}}
>
{ALL_MODELS.map((v) => (
<option value={v.name} key={v.name} disabled={!v.available}>
{v.name}
</option>
))}
</select>
</ListItem>
<ListItem
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<InputRange
value={props.modelConfig.temperature?.toFixed(1)}
min="0"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.temperature = ModalConfigValidator.temperature(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={32000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.max_tokens = ModalConfigValidator.max_tokens(
e.currentTarget.valueAsNumber,
)),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
<InputRange
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<InputRange
title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount}
min="0"
max="25"
step="1"
onChange={(e) =>
props.updateConfig(
(config) => (config.historyMessageCount = e.target.valueAsNumber),
)
}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
type="number"
min={500}
max={4000}
value={props.modelConfig.compressMessageLengthThreshold}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<input
type="checkbox"
checked={props.modelConfig.sendMemory}
onChange={(e) =>
props.updateConfig(
(config) => (config.sendMemory = e.currentTarget.checked),
)
}
></input>
</ListItem>
</List>
);
}

View File

@ -5,16 +5,6 @@
overflow: auto; overflow: auto;
} }
.settings-title {
font-size: 14px;
font-weight: bolder;
}
.settings-sub-title {
font-size: 12px;
font-weight: normal;
}
.avatar { .avatar {
cursor: pointer; cursor: pointer;
} }

View File

@ -1,7 +1,5 @@
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react"; import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
import styles from "./settings.module.scss"; import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg"; import ResetIcon from "../icons/reload.svg";
@ -10,30 +8,27 @@ import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg"; import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg"; import EditIcon from "../icons/edit.svg";
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib"; import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
import { ModelConfigList } from "./model-config";
import { IconButton } from "./button"; import { IconButton } from "./button";
import { import {
SubmitKey, SubmitKey,
useChatStore, useChatStore,
Theme, Theme,
ALL_MODELS,
useUpdateStore, useUpdateStore,
useAccessStore, useAccessStore,
ModalConfigValidator,
useAppConfig, useAppConfig,
ChatConfig,
ModelConfig,
} from "../store"; } from "../store";
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales"; import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { copyToClipboard, getEmojiUrl } from "../utils"; import { copyToClipboard } from "../utils";
import Link from "next/link"; import Link from "next/link";
import { Path, UPDATE_URL } from "../constant"; import { Path, UPDATE_URL } from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt"; import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error"; import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji";
function UserPromptModal(props: { onClose?: () => void }) { function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore(); const promptStore = usePromptStore();
@ -136,148 +131,6 @@ function UserPromptModal(props: { onClose?: () => void }) {
); );
} }
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>
{props.children}
</ListItem>
);
}
export function ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
return (
<>
<SettingItem title={Locale.Settings.Model}>
<select
value={props.modelConfig.model}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.model = ModalConfigValidator.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}
>
<InputRange
value={props.modelConfig.temperature?.toFixed(1)}
min="0"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.temperature = ModalConfigValidator.temperature(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</SettingItem>
<SettingItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={32000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.max_tokens = ModalConfigValidator.max_tokens(
e.currentTarget.valueAsNumber,
)),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.PresencePenlty.Title}
subTitle={Locale.Settings.PresencePenlty.SubTitle}
>
<InputRange
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</SettingItem>
<SettingItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<InputRange
title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount}
min="0"
max="25"
step="1"
onChange={(e) =>
props.updateConfig(
(config) => (config.historyMessageCount = e.target.valueAsNumber),
)
}
></InputRange>
</SettingItem>
<SettingItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<input
type="number"
min={500}
max={4000}
value={props.modelConfig.compressMessageLengthThreshold}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.compressMessageLengthThreshold =
e.currentTarget.valueAsNumber),
)
}
></input>
</SettingItem>
</>
);
}
export function Settings() { export function Settings() {
const navigate = useNavigate(); const navigate = useNavigate();
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@ -401,16 +254,13 @@ export function Settings() {
</div> </div>
<div className={styles["settings"]}> <div className={styles["settings"]}>
<List> <List>
<SettingItem title={Locale.Settings.Avatar}> <ListItem title={Locale.Settings.Avatar}>
<Popover <Popover
onClose={() => setShowEmojiPicker(false)} onClose={() => setShowEmojiPicker(false)}
content={ content={
<EmojiPicker <AvatarPicker
lazyLoadEmojis onEmojiClick={(avatar: string) => {
theme={EmojiTheme.AUTO} updateConfig((config) => (config.avatar = avatar));
getEmojiUrl={getEmojiUrl}
onEmojiClick={(e) => {
updateConfig((config) => (config.avatar = e.unified));
setShowEmojiPicker(false); setShowEmojiPicker(false);
}} }}
/> />
@ -421,12 +271,12 @@ export function Settings() {
className={styles.avatar} className={styles.avatar}
onClick={() => setShowEmojiPicker(true)} onClick={() => setShowEmojiPicker(true)}
> >
<Avatar role="user" /> <Avatar avatar={config.avatar} />
</div> </div>
</Popover> </Popover>
</SettingItem> </ListItem>
<SettingItem <ListItem
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")} title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
subTitle={ subTitle={
checkingUpdate checkingUpdate
@ -449,9 +299,9 @@ export function Settings() {
onClick={() => checkUpdate(true)} onClick={() => checkUpdate(true)}
/> />
)} )}
</SettingItem> </ListItem>
<SettingItem title={Locale.Settings.SendKey}> <ListItem title={Locale.Settings.SendKey}>
<select <select
value={config.submitKey} value={config.submitKey}
onChange={(e) => { onChange={(e) => {
@ -467,12 +317,9 @@ export function Settings() {
</option> </option>
))} ))}
</select> </select>
</SettingItem> </ListItem>
<ListItem> <ListItem title={Locale.Settings.Theme}>
<div className={styles["settings-title"]}>
{Locale.Settings.Theme}
</div>
<select <select
value={config.theme} value={config.theme}
onChange={(e) => { onChange={(e) => {
@ -489,7 +336,7 @@ export function Settings() {
</select> </select>
</ListItem> </ListItem>
<SettingItem title={Locale.Settings.Lang.Name}> <ListItem title={Locale.Settings.Lang.Name}>
<select <select
value={getLang()} value={getLang()}
onChange={(e) => { onChange={(e) => {
@ -502,9 +349,9 @@ export function Settings() {
</option> </option>
))} ))}
</select> </select>
</SettingItem> </ListItem>
<SettingItem <ListItem
title={Locale.Settings.FontSize.Title} title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle} subTitle={Locale.Settings.FontSize.SubTitle}
> >
@ -521,9 +368,9 @@ export function Settings() {
) )
} }
></InputRange> ></InputRange>
</SettingItem> </ListItem>
<SettingItem title={Locale.Settings.TightBorder}> <ListItem title={Locale.Settings.TightBorder}>
<input <input
type="checkbox" type="checkbox"
checked={config.tightBorder} checked={config.tightBorder}
@ -533,9 +380,9 @@ export function Settings() {
) )
} }
></input> ></input>
</SettingItem> </ListItem>
<SettingItem title={Locale.Settings.SendPreviewBubble}> <ListItem title={Locale.Settings.SendPreviewBubble}>
<input <input
type="checkbox" type="checkbox"
checked={config.sendPreviewBubble} checked={config.sendPreviewBubble}
@ -546,12 +393,12 @@ export function Settings() {
) )
} }
></input> ></input>
</SettingItem> </ListItem>
</List> </List>
<List> <List>
{enabledAccessControl ? ( {enabledAccessControl ? (
<SettingItem <ListItem
title={Locale.Settings.AccessCode.Title} title={Locale.Settings.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle} subTitle={Locale.Settings.AccessCode.SubTitle}
> >
@ -563,12 +410,12 @@ export function Settings() {
accessStore.updateCode(e.currentTarget.value); accessStore.updateCode(e.currentTarget.value);
}} }}
/> />
</SettingItem> </ListItem>
) : ( ) : (
<></> <></>
)} )}
<SettingItem <ListItem
title={Locale.Settings.Token.Title} title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle} subTitle={Locale.Settings.Token.SubTitle}
> >
@ -580,9 +427,9 @@ export function Settings() {
accessStore.updateToken(e.currentTarget.value); accessStore.updateToken(e.currentTarget.value);
}} }}
/> />
</SettingItem> </ListItem>
<SettingItem <ListItem
title={Locale.Settings.Usage.Title} title={Locale.Settings.Usage.Title}
subTitle={ subTitle={
showUsage showUsage
@ -604,11 +451,11 @@ export function Settings() {
onClick={checkUsage} onClick={checkUsage}
/> />
)} )}
</SettingItem> </ListItem>
</List> </List>
<List> <List>
<SettingItem <ListItem
title={Locale.Settings.Prompt.Disable.Title} title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle} subTitle={Locale.Settings.Prompt.Disable.SubTitle}
> >
@ -622,9 +469,9 @@ export function Settings() {
) )
} }
></input> ></input>
</SettingItem> </ListItem>
<SettingItem <ListItem
title={Locale.Settings.Prompt.List} title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount( subTitle={Locale.Settings.Prompt.ListCount(
builtinCount, builtinCount,
@ -636,19 +483,17 @@ export function Settings() {
text={Locale.Settings.Prompt.Edit} text={Locale.Settings.Prompt.Edit}
onClick={() => setShowPromptModal(true)} onClick={() => setShowPromptModal(true)}
/> />
</SettingItem> </ListItem>
</List> </List>
<List> <ModelConfigList
<ModelConfigList modelConfig={config.modelConfig}
modelConfig={config.modelConfig} updateConfig={(upater) => {
updateConfig={(upater) => { const modelConfig = { ...config.modelConfig };
const modelConfig = { ...config.modelConfig }; upater(modelConfig);
upater(modelConfig); config.update((config) => (config.modelConfig = modelConfig));
config.update((config) => (config.modelConfig = modelConfig)); }}
}} />
/>
</List>
{shouldShowPromptModal && ( {shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} /> <UserPromptModal onClose={() => setShowPromptModal(false)} />

View File

@ -35,6 +35,16 @@
border-bottom: var(--border-in-light); border-bottom: var(--border-in-light);
padding: 10px 20px; padding: 10px 20px;
animation: slide-in ease 0.6s; animation: slide-in ease 0.6s;
.list-item-title {
font-size: 14px;
font-weight: bolder;
}
.list-item-sub-title {
font-size: 12px;
font-weight: normal;
}
} }
.list { .list {
@ -89,6 +99,8 @@
padding: var(--modal-padding); padding: var(--modal-padding);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
border-top: var(--border-in-light);
box-shadow: var(--shadow);
.modal-actions { .modal-actions {
display: flex; display: flex;

View File

@ -33,12 +33,22 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
); );
} }
export function ListItem(props: { children: JSX.Element[] }) { export function ListItem(props: {
if (props.children.length > 2) { title: string;
throw Error("Only Support Two Children"); subTitle?: string;
} children?: JSX.Element | JSX.Element[];
}) {
return <div className={styles["list-item"]}>{props.children}</div>; return (
<div className={styles["list-item"]}>
<div className={styles["list-item-title"]}>
<div>{props.title}</div>
{props.subTitle && (
<div className={styles["list-item-sub-title"]}>{props.subTitle}</div>
)}
</div>
{props.children}
</div>
);
} }
export function List(props: { children: JSX.Element[] | JSX.Element }) { export function List(props: { children: JSX.Element[] | JSX.Element }) {
@ -63,7 +73,7 @@ export function Loading() {
interface ModalProps { interface ModalProps {
title: string; title: string;
children?: JSX.Element; children?: JSX.Element | JSX.Element[];
actions?: JSX.Element[]; actions?: JSX.Element[];
onClose?: () => void; onClose?: () => void;
} }

View File

@ -39,7 +39,7 @@ const cn = {
}, },
Memory: { Memory: {
Title: "历史摘要", Title: "历史摘要",
EmptyContent: "尚未总结", EmptyContent: "对话内容过短,无需总结",
Send: "启用总结并发送摘要", Send: "启用总结并发送摘要",
Copy: "复制摘要", Copy: "复制摘要",
Reset: "重置对话", Reset: "重置对话",
@ -172,8 +172,8 @@ const cn = {
}, },
Context: { Context: {
Toast: (x: any) => `已设置 ${x} 条前置上下文`, Toast: (x: any) => `已设置 ${x} 条前置上下文`,
Edit: "前置上下文和历史记忆", Edit: "对话设置",
Add: "新增一条", Add: "新增预设对话",
}, },
}; };

View File

@ -11,7 +11,7 @@ import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales"; import Locale from "../locales";
import { showToast } from "../components/ui-lib"; import { showToast } from "../components/ui-lib";
import { ModelType, useAppConfig } from "./config"; import { ModelConfig, ModelType, useAppConfig } from "./config";
export type Message = ChatCompletionResponseMessage & { export type Message = ChatCompletionResponseMessage & {
date: string; date: string;
@ -42,16 +42,18 @@ export interface ChatStat {
export interface ChatSession { export interface ChatSession {
id: number; id: number;
topic: string; topic: string;
sendMemory: boolean; avatar?: string;
memoryPrompt: string; memoryPrompt: string;
context: Message[]; context: Message[];
messages: Message[]; messages: Message[];
stat: ChatStat; stat: ChatStat;
lastUpdate: string; lastUpdate: string;
lastSummarizeIndex: number; lastSummarizeIndex: number;
modelConfig: ModelConfig;
} }
const DEFAULT_TOPIC = Locale.Store.DefaultTopic; export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
export const BOT_HELLO: Message = createMessage({ export const BOT_HELLO: Message = createMessage({
role: "assistant", role: "assistant",
content: Locale.Store.BotHello, content: Locale.Store.BotHello,
@ -63,7 +65,6 @@ function createEmptySession(): ChatSession {
return { return {
id: Date.now(), id: Date.now(),
topic: DEFAULT_TOPIC, topic: DEFAULT_TOPIC,
sendMemory: true,
memoryPrompt: "", memoryPrompt: "",
context: [], context: [],
messages: [], messages: [],
@ -74,6 +75,8 @@ function createEmptySession(): ChatSession {
}, },
lastUpdate: createDate, lastUpdate: createDate,
lastSummarizeIndex: 0, lastSummarizeIndex: 0,
modelConfig: useAppConfig.getState().modelConfig,
}; };
} }

View File

@ -32,6 +32,7 @@ const DEFAULT_CONFIG = {
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
}, },

View File

@ -1,4 +1,4 @@
export * from "./app"; export * from "./chat";
export * from "./update"; export * from "./update";
export * from "./access"; export * from "./access";
export * from "./config"; export * from "./config";

View File

@ -325,3 +325,14 @@ pre {
min-width: 80%; min-width: 80%;
} }
} }
.user-avtar {
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
border-radius: 10px;
}

View File

@ -1,4 +1,3 @@
import { EmojiStyle } from "emoji-picker-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib"; import { showToast } from "./components/ui-lib";
import Locale from "./locales"; import Locale from "./locales";
@ -90,10 +89,6 @@ export function selectOrCopy(el: HTMLElement, content: string) {
return true; return true;
} }
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}
function getDomContentWidth(dom: HTMLElement) { function getDomContentWidth(dom: HTMLElement) {
const style = window.getComputedStyle(dom); const style = window.getComputedStyle(dom);
const paddingWidth = const paddingWidth =