|
|
|
@ -14,6 +14,11 @@ import DeleteIcon from "../icons/delete.svg";
|
|
|
|
|
import MaxIcon from "../icons/max.svg";
|
|
|
|
|
import MinIcon from "../icons/min.svg";
|
|
|
|
|
|
|
|
|
|
import LightIcon from "../icons/light.svg";
|
|
|
|
|
import DarkIcon from "../icons/dark.svg";
|
|
|
|
|
import AutoIcon from "../icons/auto.svg";
|
|
|
|
|
import BottomIcon from "../icons/bottom.svg";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Message,
|
|
|
|
|
SubmitKey,
|
|
|
|
@ -22,6 +27,7 @@ import {
|
|
|
|
|
ROLES,
|
|
|
|
|
createMessage,
|
|
|
|
|
useAccessStore,
|
|
|
|
|
Theme,
|
|
|
|
|
} from "../store";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
@ -31,6 +37,7 @@ import {
|
|
|
|
|
isMobileScreen,
|
|
|
|
|
selectOrCopy,
|
|
|
|
|
autoGrowTextArea,
|
|
|
|
|
getCSSVar,
|
|
|
|
|
} from "../utils";
|
|
|
|
|
|
|
|
|
|
import dynamic from "next/dynamic";
|
|
|
|
@ -60,7 +67,11 @@ export function Avatar(props: { role: Message["role"] }) {
|
|
|
|
|
const config = useChatStore((state) => state.config);
|
|
|
|
|
|
|
|
|
|
if (props.role !== "user") {
|
|
|
|
|
return <BotIcon className={styles["user-avtar"]} />;
|
|
|
|
|
return (
|
|
|
|
|
<div className="no-dark">
|
|
|
|
|
<BotIcon className={styles["user-avtar"]} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
@ -316,22 +327,78 @@ function useScrollToBottom() {
|
|
|
|
|
// for auto-scroll
|
|
|
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
|
|
|
|
|
|
|
|
// auto scroll
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
const scrollToBottom = () => {
|
|
|
|
|
const dom = scrollRef.current;
|
|
|
|
|
if (dom && autoScroll) {
|
|
|
|
|
if (dom) {
|
|
|
|
|
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// auto scroll
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
autoScroll && scrollToBottom();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
scrollRef,
|
|
|
|
|
autoScroll,
|
|
|
|
|
setAutoScroll,
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ChatActions(props: {
|
|
|
|
|
showPromptModal: () => void;
|
|
|
|
|
scrollToBottom: () => void;
|
|
|
|
|
hitBottom: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
const chatStore = useChatStore();
|
|
|
|
|
|
|
|
|
|
const theme = chatStore.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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={chatStyle["chat-input-actions"]}>
|
|
|
|
|
{!props.hitBottom && (
|
|
|
|
|
<div
|
|
|
|
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
|
|
onClick={props.scrollToBottom}
|
|
|
|
|
>
|
|
|
|
|
<BottomIcon />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{props.hitBottom && (
|
|
|
|
|
<div
|
|
|
|
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
|
|
onClick={props.showPromptModal}
|
|
|
|
|
>
|
|
|
|
|
<BrainIcon />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
|
|
onClick={nextTheme}
|
|
|
|
|
>
|
|
|
|
|
{theme === Theme.Auto ? (
|
|
|
|
|
<AutoIcon />
|
|
|
|
|
) : theme === Theme.Light ? (
|
|
|
|
|
<LightIcon />
|
|
|
|
|
) : theme === Theme.Dark ? (
|
|
|
|
|
<DarkIcon />
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function Chat(props: {
|
|
|
|
|
showSideBar?: () => void;
|
|
|
|
|
sideBarShowing?: boolean;
|
|
|
|
@ -350,7 +417,7 @@ export function Chat(props: {
|
|
|
|
|
const [beforeInput, setBeforeInput] = useState("");
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
|
|
|
|
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
|
|
|
|
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
|
|
|
|
const [hitBottom, setHitBottom] = useState(false);
|
|
|
|
|
|
|
|
|
|
const onChatBodyScroll = (e: HTMLElement) => {
|
|
|
|
@ -672,22 +739,20 @@ export function Chat(props: {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{(message.preview || message.content.length === 0) &&
|
|
|
|
|
!isUser ? (
|
|
|
|
|
<LoadingIcon />
|
|
|
|
|
) : (
|
|
|
|
|
<div
|
|
|
|
|
className="markdown-body"
|
|
|
|
|
style={{ fontSize: `${fontSize}px` }}
|
|
|
|
|
<Markdown
|
|
|
|
|
content={message.content}
|
|
|
|
|
loading={
|
|
|
|
|
(message.preview || message.content.length === 0) &&
|
|
|
|
|
!isUser
|
|
|
|
|
}
|
|
|
|
|
onContextMenu={(e) => onRightClick(e, message)}
|
|
|
|
|
onDoubleClickCapture={() => {
|
|
|
|
|
if (!isMobileScreen()) return;
|
|
|
|
|
setUserInput(message.content);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Markdown content={message.content} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
fontSize={fontSize}
|
|
|
|
|
parentRef={scrollRef}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{!isUser && !message.preview && (
|
|
|
|
|
<div className={styles["chat-message-actions"]}>
|
|
|
|
@ -704,6 +769,12 @@ export function Chat(props: {
|
|
|
|
|
|
|
|
|
|
<div className={styles["chat-input-panel"]}>
|
|
|
|
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
|
|
|
|
|
|
|
|
|
<ChatActions
|
|
|
|
|
showPromptModal={() => setShowPromptModal(true)}
|
|
|
|
|
scrollToBottom={scrollToBottom}
|
|
|
|
|
hitBottom={hitBottom}
|
|
|
|
|
/>
|
|
|
|
|
<div className={styles["chat-input-panel-inner"]}>
|
|
|
|
|
<textarea
|
|
|
|
|
ref={inputRef}
|
|
|
|
|