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

@@ -1,4 +1,4 @@
import { useDebounce, useDebouncedCallback } from "use-debounce";
import { useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
@@ -9,8 +9,6 @@ import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg";
import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import MaxIcon from "../icons/max.svg";
@@ -33,12 +31,13 @@ import {
Theme,
ModelType,
useAppConfig,
ModelConfig,
DEFAULT_TOPIC,
} from "../store";
import {
copyToClipboard,
downloadAs,
getEmojiUrl,
selectOrCopy,
autoGrowTextArea,
useMobileScreen,
@@ -54,10 +53,11 @@ import { IconButton } from "./button";
import styles from "./home.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 { Path } from "../constant";
import { ModelConfigList } from "./model-config";
import { AvatarPicker } from "./emoji";
const Markdown = dynamic(
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 />,
});
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) {
const mdText =
`# ${topic}\n\n` +
@@ -129,15 +107,13 @@ function exportMessages(messages: Message[], topic: string) {
});
}
function PromptToast(props: {
showToast?: boolean;
showModal?: boolean;
setShowModal: (_: boolean) => void;
}) {
function ContextPrompts() {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const context = session.context;
const [showPicker, setShowPicker] = useState(false);
const addContextPrompt = (prompt: Message) => {
chatStore.updateCurrentSession((session) => {
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 (
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
{props.showToast && (
@@ -171,115 +306,7 @@ function PromptToast(props: {
</div>
)}
{props.showModal && (
<div className="modal-mask">
<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>
<SessionConfigModel onClose={() => props.setShowModal(false)} />
)}
</div>
);
@@ -654,7 +681,7 @@ export function Chat() {
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClickCapture={renameSession}
>
{session.topic}
{!session.topic ? DEFAULT_TOPIC : session.topic}
</div>
<div className={styles["window-header-sub-title"]}>
{Locale.Chat.SubTitle(session.messages.length)}
@@ -739,7 +766,13 @@ export function Chat() {
>
<div className={styles["chat-message-container"]}>
<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>
{showTyping && (
<div className={styles["chat-message-status"]}>