feat: #138 add context prompt, close #330 #321

This commit is contained in:
Yifei Zhang
2023-04-02 17:48:43 +00:00
parent c978de2c10
commit b85245e317
14 changed files with 296 additions and 69 deletions

View File

@@ -9,6 +9,8 @@ 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 AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import {
Message,
@@ -16,6 +18,7 @@ import {
useChatStore,
ChatSession,
BOT_HELLO,
ROLES,
} from "../store";
import {
@@ -33,8 +36,9 @@ import Locale from "../locales";
import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
import { showModal, showToast } from "./ui-lib";
import { Modal, showModal, showToast } from "./ui-lib";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -94,26 +98,130 @@ function exportMessages(messages: Message[], topic: string) {
});
}
function showMemoryPrompt(session: ChatSession) {
showModal({
title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
children: (
<div className="markdown-body">
<pre className={styles["export-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
</pre>
function PromptToast(props: {
showModal: boolean;
setShowModal: (_: boolean) => void;
}) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const context = session.context;
const addContextPrompt = (prompt: Message) => {
chatStore.updateCurrentSession((session) => {
session.context.push(prompt);
});
};
const removeContextPrompt = (i: number) => {
chatStore.updateCurrentSession((session) => {
session.context.splice(i, 1);
});
};
const updateContextPrompt = (i: number, prompt: Message) => {
chatStore.updateCurrentSession((session) => {
session.context[i] = prompt;
});
};
return (
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
<div
className={chatStyle["prompt-toast-inner"] + " clickable"}
role="button"
onClick={() => props.setShowModal(true)}
>
<BrainIcon />
<span className={chatStyle["prompt-toast-content"]}>
{context.length}
</span>
</div>
),
actions: [
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Copy}
onClick={() => copyToClipboard(session.memoryPrompt)}
/>,
],
});
{props.showModal && (
<div className="modal-mask">
<Modal
title="编辑前置上下文"
onClose={() => props.setShowModal(false)}
actions={[
<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"]}
onChange={(e) =>
updateContextPrompt(i, {
...c,
content: e.target.value as any,
})
}
></input>
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
onClick={() => removeContextPrompt(i)}
/>
</div>
))}
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text="新增"
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"]}>
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
{session.messages.length})
</div>
<div className={chatStyle["memory-prompt-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
</div>
</div>
</>
</Modal>
</div>
)}
</div>
);
}
function useSubmitHandler() {
@@ -172,9 +280,8 @@ function useScrollToBottom() {
// auto scroll
useLayoutEffect(() => {
const dom = scrollRef.current;
if (dom && autoScroll) {
dom.scrollTop = dom.scrollHeight;
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 500);
}
});
@@ -243,8 +350,12 @@ export function Chat(props: {
setPromptHints([]);
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion
if (text.startsWith("/") && text.length > 1) {
onSearch(text.slice(1));
if (text.startsWith("/")) {
let searchText = text.slice(1);
if (searchText.length === 0) {
searchText = " ";
}
onSearch(searchText);
}
}
};
@@ -299,8 +410,18 @@ export function Chat(props: {
const config = useChatStore((state) => state.config);
const context: RenderMessage[] = session.context.slice();
if (
context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content
) {
context.push(BOT_HELLO);
}
// preview messages
const messages = (session.messages as RenderMessage[])
const messages = context
.concat(session.messages as RenderMessage[])
.concat(
isLoading
? [
@@ -326,6 +447,8 @@ export function Chat(props: {
: [],
);
const [showPromptModal, setShowPromptModal] = useState(false);
return (
<div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}>
@@ -365,7 +488,7 @@ export function Chat(props: {
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
showMemoryPrompt(session);
setShowPromptModal(true);
}}
/>
</div>
@@ -380,6 +503,11 @@ export function Chat(props: {
/>
</div>
</div>
<PromptToast
showModal={showPromptModal}
setShowModal={setShowPromptModal}
/>
</div>
<div className={styles["chat-body"]} ref={scrollRef}>
@@ -402,7 +530,10 @@ export function Chat(props: {
{Locale.Chat.Typing}
</div>
)}
<div className={styles["chat-message-item"]}>
<div
className={styles["chat-message-item"]}
onMouseOver={() => inputRef.current?.blur()}
>
{!isUser &&
!(message.preview || message.content.length === 0) && (
<div className={styles["chat-message-top-actions"]}>
@@ -467,7 +598,7 @@ export function Chat(props: {
ref={inputRef}
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
rows={4}
rows={2}
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}