|
@@ -1,5 +1,5 @@
|
|
|
-import { useDebounce, useDebouncedCallback } from "use-debounce";
|
|
|
-import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
|
|
+import { useDebouncedCallback } from "use-debounce";
|
|
|
+import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
|
|
|
|
|
import SendWhiteIcon from "../icons/send-white.svg";
|
|
|
import BrainIcon from "../icons/brain.svg";
|
|
@@ -9,11 +9,11 @@ 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 AddIcon from "../icons/add.svg";
|
|
|
-import DeleteIcon from "../icons/delete.svg";
|
|
|
+import PromptIcon from "../icons/prompt.svg";
|
|
|
+import MaskIcon from "../icons/mask.svg";
|
|
|
import MaxIcon from "../icons/max.svg";
|
|
|
import MinIcon from "../icons/min.svg";
|
|
|
+import ResetIcon from "../icons/reload.svg";
|
|
|
|
|
|
import LightIcon from "../icons/light.svg";
|
|
|
import DarkIcon from "../icons/dark.svg";
|
|
@@ -26,19 +26,19 @@ import {
|
|
|
SubmitKey,
|
|
|
useChatStore,
|
|
|
BOT_HELLO,
|
|
|
- ROLES,
|
|
|
createMessage,
|
|
|
useAccessStore,
|
|
|
Theme,
|
|
|
+ useAppConfig,
|
|
|
+ DEFAULT_TOPIC,
|
|
|
} from "../store";
|
|
|
|
|
|
import {
|
|
|
copyToClipboard,
|
|
|
downloadAs,
|
|
|
- getEmojiUrl,
|
|
|
- isMobileScreen,
|
|
|
selectOrCopy,
|
|
|
autoGrowTextArea,
|
|
|
+ useMobileScreen,
|
|
|
} from "../utils";
|
|
|
|
|
|
import dynamic from "next/dynamic";
|
|
@@ -51,37 +51,18 @@ import { IconButton } from "./button";
|
|
|
import styles from "./home.module.scss";
|
|
|
import chatStyle from "./chat.module.scss";
|
|
|
|
|
|
-import { Input, Modal, showModal } from "./ui-lib";
|
|
|
+import { ListItem, Modal, showModal } from "./ui-lib";
|
|
|
+import { useLocation, useNavigate } from "react-router-dom";
|
|
|
+import { LAST_INPUT_KEY, Path } from "../constant";
|
|
|
+import { Avatar } from "./emoji";
|
|
|
+import { MaskAvatar, MaskConfig } from "./mask";
|
|
|
+import { useMaskStore } from "../store/mask";
|
|
|
+import { useCommand } from "../command";
|
|
|
|
|
|
-const Markdown = dynamic(
|
|
|
- async () => memo((await import("./markdown")).Markdown),
|
|
|
- {
|
|
|
- loading: () => <LoadingIcon />,
|
|
|
- },
|
|
|
-);
|
|
|
-
|
|
|
-const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
|
|
+const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|
|
loading: () => <LoadingIcon />,
|
|
|
});
|
|
|
|
|
|
-export function Avatar(props: { role: Message["role"] }) {
|
|
|
- const config = useChatStore((state) => state.config);
|
|
|
-
|
|
|
- if (props.role !== "user") {
|
|
|
- return (
|
|
|
- <div className="no-dark">
|
|
|
- <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` +
|
|
@@ -120,6 +101,64 @@ function exportMessages(messages: Message[], topic: string) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+export function SessionConfigModel(props: { onClose: () => void }) {
|
|
|
+ const chatStore = useChatStore();
|
|
|
+ const session = chatStore.currentSession();
|
|
|
+ const maskStore = useMaskStore();
|
|
|
+ const navigate = useNavigate();
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="modal-mask">
|
|
|
+ <Modal
|
|
|
+ title={Locale.Context.Edit}
|
|
|
+ onClose={() => props.onClose()}
|
|
|
+ actions={[
|
|
|
+ <IconButton
|
|
|
+ key="reset"
|
|
|
+ icon={<ResetIcon />}
|
|
|
+ bordered
|
|
|
+ text={Locale.Chat.Config.Reset}
|
|
|
+ onClick={() =>
|
|
|
+ confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
|
|
|
+ }
|
|
|
+ />,
|
|
|
+ <IconButton
|
|
|
+ key="copy"
|
|
|
+ icon={<CopyIcon />}
|
|
|
+ bordered
|
|
|
+ text={Locale.Chat.Config.SaveAs}
|
|
|
+ onClick={() => {
|
|
|
+ navigate(Path.Masks);
|
|
|
+ setTimeout(() => {
|
|
|
+ maskStore.create(session.mask);
|
|
|
+ }, 500);
|
|
|
+ }}
|
|
|
+ />,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <MaskConfig
|
|
|
+ mask={session.mask}
|
|
|
+ updateMask={(updater) => {
|
|
|
+ const mask = { ...session.mask };
|
|
|
+ updater(mask);
|
|
|
+ chatStore.updateCurrentSession((session) => (session.mask = mask));
|
|
|
+ }}
|
|
|
+ extraListItems={
|
|
|
+ session.mask.modelConfig.sendMemory ? (
|
|
|
+ <ListItem
|
|
|
+ title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
|
|
|
+ subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
|
|
+ ></ListItem>
|
|
|
+ ) : (
|
|
|
+ <></>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ ></MaskConfig>
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function PromptToast(props: {
|
|
|
showToast?: boolean;
|
|
|
showModal?: boolean;
|
|
@@ -127,25 +166,7 @@ function PromptToast(props: {
|
|
|
}) {
|
|
|
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;
|
|
|
- });
|
|
|
- };
|
|
|
+ const context = session.mask.context;
|
|
|
|
|
|
return (
|
|
|
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
|
@@ -162,122 +183,14 @@ 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>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function useSubmitHandler() {
|
|
|
- const config = useChatStore((state) => state.config);
|
|
|
+ const config = useAppConfig();
|
|
|
const submitKey = config.submitKey;
|
|
|
|
|
|
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -306,15 +219,63 @@ export function PromptHints(props: {
|
|
|
prompts: Prompt[];
|
|
|
onPromptSelect: (prompt: Prompt) => void;
|
|
|
}) {
|
|
|
- if (props.prompts.length === 0) return null;
|
|
|
+ const noPrompts = props.prompts.length === 0;
|
|
|
+ const [selectIndex, setSelectIndex] = useState(0);
|
|
|
+ const selectedRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ setSelectIndex(0);
|
|
|
+ }, [props.prompts.length]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const onKeyDown = (e: KeyboardEvent) => {
|
|
|
+ if (noPrompts) return;
|
|
|
+
|
|
|
+ // arrow up / down to select prompt
|
|
|
+ const changeIndex = (delta: number) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ const nextIndex = Math.max(
|
|
|
+ 0,
|
|
|
+ Math.min(props.prompts.length - 1, selectIndex + delta),
|
|
|
+ );
|
|
|
+ setSelectIndex(nextIndex);
|
|
|
+ selectedRef.current?.scrollIntoView({
|
|
|
+ block: "center",
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ if (e.key === "ArrowUp") {
|
|
|
+ changeIndex(1);
|
|
|
+ } else if (e.key === "ArrowDown") {
|
|
|
+ changeIndex(-1);
|
|
|
+ } else if (e.key === "Enter") {
|
|
|
+ const selectedPrompt = props.prompts.at(selectIndex);
|
|
|
+ if (selectedPrompt) {
|
|
|
+ props.onPromptSelect(selectedPrompt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ window.addEventListener("keydown", onKeyDown);
|
|
|
+
|
|
|
+ return () => window.removeEventListener("keydown", onKeyDown);
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, [noPrompts, selectIndex]);
|
|
|
+
|
|
|
+ if (noPrompts) return null;
|
|
|
return (
|
|
|
<div className={styles["prompt-hints"]}>
|
|
|
{props.prompts.map((prompt, i) => (
|
|
|
<div
|
|
|
- className={styles["prompt-hint"]}
|
|
|
+ ref={i === selectIndex ? selectedRef : null}
|
|
|
+ className={
|
|
|
+ styles["prompt-hint"] +
|
|
|
+ ` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}`
|
|
|
+ }
|
|
|
key={prompt.title + i.toString()}
|
|
|
onClick={() => props.onPromptSelect(prompt)}
|
|
|
+ onMouseEnter={() => setSelectIndex(i)}
|
|
|
>
|
|
|
<div className={styles["hint-title"]}>{prompt.title}</div>
|
|
|
<div className={styles["hint-content"]}>{prompt.content}</div>
|
|
@@ -351,18 +312,20 @@ function useScrollToBottom() {
|
|
|
export function ChatActions(props: {
|
|
|
showPromptModal: () => void;
|
|
|
scrollToBottom: () => void;
|
|
|
+ showPromptHints: () => void;
|
|
|
hitBottom: boolean;
|
|
|
}) {
|
|
|
- const chatStore = useChatStore();
|
|
|
+ const config = useAppConfig();
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
|
// switch themes
|
|
|
- const theme = chatStore.config.theme;
|
|
|
+ const theme = config.theme;
|
|
|
function nextTheme() {
|
|
|
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
|
|
const themeIndex = themes.indexOf(theme);
|
|
|
const nextIndex = (themeIndex + 1) % themes.length;
|
|
|
const nextTheme = themes[nextIndex];
|
|
|
- chatStore.updateConfig((config) => (config.theme = nextTheme));
|
|
|
+ config.update((config) => (config.theme = nextTheme));
|
|
|
}
|
|
|
|
|
|
// stop all responses
|
|
@@ -408,14 +371,27 @@ export function ChatActions(props: {
|
|
|
<DarkIcon />
|
|
|
) : null}
|
|
|
</div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ onClick={props.showPromptHints}
|
|
|
+ >
|
|
|
+ <PromptIcon />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ onClick={() => {
|
|
|
+ navigate(Path.Masks);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <MaskIcon />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-export function Chat(props: {
|
|
|
- showSideBar?: () => void;
|
|
|
- sideBarShowing?: boolean;
|
|
|
-}) {
|
|
|
+export function Chat() {
|
|
|
type RenderMessage = Message & { preview?: boolean };
|
|
|
|
|
|
const chatStore = useChatStore();
|
|
@@ -423,18 +399,20 @@ export function Chat(props: {
|
|
|
state.currentSession(),
|
|
|
state.currentSessionIndex,
|
|
|
]);
|
|
|
- const fontSize = useChatStore((state) => state.config.fontSize);
|
|
|
+ const config = useAppConfig();
|
|
|
+ const fontSize = config.fontSize;
|
|
|
|
|
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
|
const [userInput, setUserInput] = useState("");
|
|
|
- const [beforeInput, setBeforeInput] = useState("");
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
|
|
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
|
|
- const [hitBottom, setHitBottom] = useState(false);
|
|
|
+ const [hitBottom, setHitBottom] = useState(true);
|
|
|
+ const isMobileScreen = useMobileScreen();
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
|
const onChatBodyScroll = (e: HTMLElement) => {
|
|
|
- const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
|
|
+ const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 100;
|
|
|
setHitBottom(isTouchBottom);
|
|
|
};
|
|
|
|
|
@@ -450,9 +428,9 @@ export function Chat(props: {
|
|
|
);
|
|
|
|
|
|
const onPromptSelect = (prompt: Prompt) => {
|
|
|
- setUserInput(prompt.content);
|
|
|
setPromptHints([]);
|
|
|
inputRef.current?.focus();
|
|
|
+ setTimeout(() => setUserInput(prompt.content), 60);
|
|
|
};
|
|
|
|
|
|
// auto grow input
|
|
@@ -461,8 +439,8 @@ export function Chat(props: {
|
|
|
() => {
|
|
|
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
|
|
const inputRows = Math.min(
|
|
|
- 5,
|
|
|
- Math.max(2 + Number(!isMobileScreen()), rows),
|
|
|
+ 20,
|
|
|
+ Math.max(2 + Number(!isMobileScreen), rows),
|
|
|
);
|
|
|
setInputRows(inputRows);
|
|
|
},
|
|
@@ -485,7 +463,7 @@ export function Chat(props: {
|
|
|
// clear search results
|
|
|
if (n === 0) {
|
|
|
setPromptHints([]);
|
|
|
- } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
|
|
+ } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
|
|
// check if need to trigger auto completion
|
|
|
if (text.startsWith("/")) {
|
|
|
let searchText = text.slice(1);
|
|
@@ -494,15 +472,14 @@ export function Chat(props: {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // submit user input
|
|
|
- const onUserSubmit = () => {
|
|
|
- if (userInput.length <= 0) return;
|
|
|
+ const doSubmit = (userInput: string) => {
|
|
|
+ if (userInput.trim() === "") return;
|
|
|
setIsLoading(true);
|
|
|
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
|
|
- setBeforeInput(userInput);
|
|
|
+ localStorage.setItem(LAST_INPUT_KEY, userInput);
|
|
|
setUserInput("");
|
|
|
setPromptHints([]);
|
|
|
- if (!isMobileScreen()) inputRef.current?.focus();
|
|
|
+ if (!isMobileScreen) inputRef.current?.focus();
|
|
|
setAutoScroll(true);
|
|
|
};
|
|
|
|
|
@@ -513,30 +490,25 @@ export function Chat(props: {
|
|
|
|
|
|
// check if should send message
|
|
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
|
- // if ArrowUp and no userInput
|
|
|
+ // if ArrowUp and no userInput, fill with last input
|
|
|
if (e.key === "ArrowUp" && userInput.length <= 0) {
|
|
|
- setUserInput(beforeInput);
|
|
|
+ setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
|
|
e.preventDefault();
|
|
|
return;
|
|
|
}
|
|
|
if (shouldSubmit(e)) {
|
|
|
- onUserSubmit();
|
|
|
+ doSubmit(userInput);
|
|
|
e.preventDefault();
|
|
|
}
|
|
|
};
|
|
|
const onRightClick = (e: any, message: Message) => {
|
|
|
- // auto fill user input
|
|
|
- if (message.role === "user") {
|
|
|
- setUserInput(message.content);
|
|
|
- }
|
|
|
-
|
|
|
// copy to clipboard
|
|
|
if (selectOrCopy(e.currentTarget, message.content)) {
|
|
|
e.preventDefault();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const findLastUesrIndex = (messageId: number) => {
|
|
|
+ const findLastUserIndex = (messageId: number) => {
|
|
|
// find last user input message and resend
|
|
|
let lastUserMessageIndex: number | null = null;
|
|
|
for (let i = 0; i < session.messages.length; i += 1) {
|
|
@@ -559,14 +531,14 @@ export function Chat(props: {
|
|
|
};
|
|
|
|
|
|
const onDelete = (botMessageId: number) => {
|
|
|
- const userIndex = findLastUesrIndex(botMessageId);
|
|
|
+ const userIndex = findLastUserIndex(botMessageId);
|
|
|
if (userIndex === null) return;
|
|
|
deleteMessage(userIndex);
|
|
|
};
|
|
|
|
|
|
const onResend = (botMessageId: number) => {
|
|
|
// find last user input message and resend
|
|
|
- const userIndex = findLastUesrIndex(botMessageId);
|
|
|
+ const userIndex = findLastUserIndex(botMessageId);
|
|
|
if (userIndex === null) return;
|
|
|
|
|
|
setIsLoading(true);
|
|
@@ -576,9 +548,7 @@ export function Chat(props: {
|
|
|
inputRef.current?.focus();
|
|
|
};
|
|
|
|
|
|
- const config = useChatStore((state) => state.config);
|
|
|
-
|
|
|
- const context: RenderMessage[] = session.context.slice();
|
|
|
+ const context: RenderMessage[] = session.mask.context.slice();
|
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
@@ -632,44 +602,48 @@ export function Chat(props: {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // Auto focus
|
|
|
- useEffect(() => {
|
|
|
- if (props.sideBarShowing && isMobileScreen()) return;
|
|
|
- inputRef.current?.focus();
|
|
|
- // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
- }, []);
|
|
|
+ const location = useLocation();
|
|
|
+ const isChat = location.pathname === Path.Chat;
|
|
|
+ const autoFocus = !isMobileScreen || isChat; // only focus in chat page
|
|
|
+
|
|
|
+ useCommand({
|
|
|
+ fill: setUserInput,
|
|
|
+ submit: (text) => {
|
|
|
+ doSubmit(text);
|
|
|
+ },
|
|
|
+ });
|
|
|
|
|
|
return (
|
|
|
<div className={styles.chat} key={session.id}>
|
|
|
- <div className={styles["window-header"]}>
|
|
|
- <div className={styles["window-header-title"]}>
|
|
|
+ <div className="window-header">
|
|
|
+ <div className="window-header-title">
|
|
|
<div
|
|
|
- className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
|
|
+ className={`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"]}>
|
|
|
+ <div className="window-header-sub-title">
|
|
|
{Locale.Chat.SubTitle(session.messages.length)}
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className={styles["window-actions"]}>
|
|
|
- <div className={styles["window-action-button"] + " " + styles.mobile}>
|
|
|
+ <div className="window-actions">
|
|
|
+ <div className={"window-action-button" + " " + styles.mobile}>
|
|
|
<IconButton
|
|
|
icon={<ReturnIcon />}
|
|
|
bordered
|
|
|
title={Locale.Chat.Actions.ChatList}
|
|
|
- onClick={props?.showSideBar}
|
|
|
+ onClick={() => navigate(Path.Home)}
|
|
|
/>
|
|
|
</div>
|
|
|
- <div className={styles["window-action-button"]}>
|
|
|
+ <div className="window-action-button">
|
|
|
<IconButton
|
|
|
icon={<RenameIcon />}
|
|
|
bordered
|
|
|
onClick={renameSession}
|
|
|
/>
|
|
|
</div>
|
|
|
- <div className={styles["window-action-button"]}>
|
|
|
+ <div className="window-action-button">
|
|
|
<IconButton
|
|
|
icon={<ExportIcon />}
|
|
|
bordered
|
|
@@ -682,13 +656,13 @@ export function Chat(props: {
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
- {!isMobileScreen() && (
|
|
|
- <div className={styles["window-action-button"]}>
|
|
|
+ {!isMobileScreen && (
|
|
|
+ <div className="window-action-button">
|
|
|
<IconButton
|
|
|
- icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
|
|
+ icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
|
|
bordered
|
|
|
onClick={() => {
|
|
|
- chatStore.updateConfig(
|
|
|
+ config.update(
|
|
|
(config) => (config.tightBorder = !config.tightBorder),
|
|
|
);
|
|
|
}}
|
|
@@ -717,6 +691,11 @@ export function Chat(props: {
|
|
|
>
|
|
|
{messages.map((message, i) => {
|
|
|
const isUser = message.role === "user";
|
|
|
+ const showActions =
|
|
|
+ !isUser &&
|
|
|
+ i > 0 &&
|
|
|
+ !(message.preview || message.content.length === 0);
|
|
|
+ const showTyping = message.preview || message.streaming;
|
|
|
|
|
|
return (
|
|
|
<div
|
|
@@ -727,49 +706,52 @@ export function Chat(props: {
|
|
|
>
|
|
|
<div className={styles["chat-message-container"]}>
|
|
|
<div className={styles["chat-message-avatar"]}>
|
|
|
- <Avatar role={message.role} />
|
|
|
+ {message.role === "user" ? (
|
|
|
+ <Avatar avatar={config.avatar} />
|
|
|
+ ) : (
|
|
|
+ <MaskAvatar mask={session.mask} />
|
|
|
+ )}
|
|
|
</div>
|
|
|
- {(message.preview || message.streaming) && (
|
|
|
+ {showTyping && (
|
|
|
<div className={styles["chat-message-status"]}>
|
|
|
{Locale.Chat.Typing}
|
|
|
</div>
|
|
|
)}
|
|
|
<div className={styles["chat-message-item"]}>
|
|
|
- {!isUser &&
|
|
|
- !(message.preview || message.content.length === 0) && (
|
|
|
- <div className={styles["chat-message-top-actions"]}>
|
|
|
- {message.streaming ? (
|
|
|
- <div
|
|
|
- className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => onUserStop(message.id ?? i)}
|
|
|
- >
|
|
|
- {Locale.Chat.Actions.Stop}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- <div
|
|
|
- className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => onDelete(message.id ?? i)}
|
|
|
- >
|
|
|
- {Locale.Chat.Actions.Delete}
|
|
|
- </div>
|
|
|
- <div
|
|
|
- className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => onResend(message.id ?? i)}
|
|
|
- >
|
|
|
- {Locale.Chat.Actions.Retry}
|
|
|
- </div>
|
|
|
- </>
|
|
|
- )}
|
|
|
-
|
|
|
+ {showActions && (
|
|
|
+ <div className={styles["chat-message-top-actions"]}>
|
|
|
+ {message.streaming ? (
|
|
|
<div
|
|
|
className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => copyToClipboard(message.content)}
|
|
|
+ onClick={() => onUserStop(message.id ?? i)}
|
|
|
>
|
|
|
- {Locale.Chat.Actions.Copy}
|
|
|
+ {Locale.Chat.Actions.Stop}
|
|
|
</div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className={styles["chat-message-top-action"]}
|
|
|
+ onClick={() => onDelete(message.id ?? i)}
|
|
|
+ >
|
|
|
+ {Locale.Chat.Actions.Delete}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ className={styles["chat-message-top-action"]}
|
|
|
+ onClick={() => onResend(message.id ?? i)}
|
|
|
+ >
|
|
|
+ {Locale.Chat.Actions.Retry}
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div
|
|
|
+ className={styles["chat-message-top-action"]}
|
|
|
+ onClick={() => copyToClipboard(message.content)}
|
|
|
+ >
|
|
|
+ {Locale.Chat.Actions.Copy}
|
|
|
</div>
|
|
|
- )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
<Markdown
|
|
|
content={message.content}
|
|
|
loading={
|
|
@@ -778,11 +760,12 @@ export function Chat(props: {
|
|
|
}
|
|
|
onContextMenu={(e) => onRightClick(e, message)}
|
|
|
onDoubleClickCapture={() => {
|
|
|
- if (!isMobileScreen()) return;
|
|
|
+ if (!isMobileScreen) return;
|
|
|
setUserInput(message.content);
|
|
|
}}
|
|
|
fontSize={fontSize}
|
|
|
parentRef={scrollRef}
|
|
|
+ defaultShow={i >= messages.length - 10}
|
|
|
/>
|
|
|
</div>
|
|
|
{!isUser && !message.preview && (
|
|
@@ -805,6 +788,10 @@ export function Chat(props: {
|
|
|
showPromptModal={() => setShowPromptModal(true)}
|
|
|
scrollToBottom={scrollToBottom}
|
|
|
hitBottom={hitBottom}
|
|
|
+ showPromptHints={() => {
|
|
|
+ inputRef.current?.focus();
|
|
|
+ onSearch("");
|
|
|
+ }}
|
|
|
/>
|
|
|
<div className={styles["chat-input-panel-inner"]}>
|
|
|
<textarea
|
|
@@ -815,19 +802,16 @@ export function Chat(props: {
|
|
|
value={userInput}
|
|
|
onKeyDown={onInputKeyDown}
|
|
|
onFocus={() => setAutoScroll(true)}
|
|
|
- onBlur={() => {
|
|
|
- setAutoScroll(false);
|
|
|
- setTimeout(() => setPromptHints([]), 500);
|
|
|
- }}
|
|
|
- autoFocus={!props?.sideBarShowing}
|
|
|
+ onBlur={() => setAutoScroll(false)}
|
|
|
rows={inputRows}
|
|
|
+ autoFocus={autoFocus}
|
|
|
/>
|
|
|
<IconButton
|
|
|
icon={<SendWhiteIcon />}
|
|
|
text={Locale.Chat.Send}
|
|
|
className={styles["chat-input-send"]}
|
|
|
- noDark
|
|
|
- onClick={onUserSubmit}
|
|
|
+ type="primary"
|
|
|
+ onClick={() => doSubmit(userInput)}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|