From eedd81ee8452419653e27df0a7bfd6d42dbfcc79 Mon Sep 17 00:00:00 2001 From: lisiur Date: Wed, 15 Mar 2023 13:44:55 +0800 Subject: [PATCH] feat: support auto scroll support auto scroll fix #2 --- gui/web/src/components/Chat.tsx | 62 +++++++++++++++++++++++++---- gui/web/src/hooks/scroll.ts | 69 +++++++++++++++++++++++++++++++++ gui/web/src/models/chat.ts | 17 +++++--- 3 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 gui/web/src/hooks/scroll.ts diff --git a/gui/web/src/components/Chat.tsx b/gui/web/src/components/Chat.tsx index 7cffdad..f070a45 100644 --- a/gui/web/src/components/Chat.tsx +++ b/gui/web/src/components/Chat.tsx @@ -1,4 +1,12 @@ -import { defineComponent, PropType, ref } from "vue"; +import { + computed, + defineComponent, + onBeforeUnmount, + nextTick, + PropType, + ref, + onMounted, +} from "vue"; import mdRender from "../utils/mdRender"; import assistantAvatar from "../assets/assistant_avatar.png"; import userAvatar from "../assets/user_avatar.png"; @@ -18,6 +26,7 @@ import { useVersion } from "../hooks/version"; import { AngleDoubleUp } from "@vicons/fa"; import { dialog, message } from "../utils/prompt"; import { Chat } from "../models/chat"; +import { useAutoScroll } from "../hooks/scroll"; export default defineComponent({ props: { @@ -27,9 +36,29 @@ export default defineComponent({ }, }, setup(props) { + const scrollRef = ref>(); const inputRef = ref(); const { isComposition } = useComposition(inputRef); + const scrollEle = computed(() => { + return scrollRef.value?.$el.nextSibling.children[0] as HTMLElement; + }); + + const { + start: startAutoScroll, + stop: stopAutoScroll, + destroy: destroyAutoScroll, + scrollToBottom, + } = useAutoScroll(scrollEle); + + onMounted(() => { + nextTick(scrollToBottom); + }); + + onBeforeUnmount(() => { + destroyAutoScroll(); + }); + const { version, hasNewVersion, installNewVersion, newVersion, relaunch } = useVersion(); const { checkApiKey: check_api_key, setProxy, setApiKey } = useConfig(); @@ -42,7 +71,12 @@ export default defineComponent({ if (e.key === "Enter" && !e.ctrlKey && !isComposition.value) { const message = prompt.value; prompt.value = ""; - props.chat.sendMessage(message); + props.chat.sendMessage(message, { + onFinish: stopAutoScroll, + }); + setTimeout(() => { + startAutoScroll(); + }, 20); e.preventDefault(); } } @@ -105,10 +139,14 @@ export default defineComponent({ style="background-color: var(--body-color)" >
- +
{props.chat.messages.map((message, index) => ( -
{renderMessage(message, props.chat)}
+
+ {renderMessage(message, props.chat, { + onFinish: stopAutoScroll, + })} +
))}
@@ -212,11 +250,15 @@ function renderTriangle( } } -function renderMessage(message: Message, chat: Chat) { +function renderMessage( + message: Message, + chat: Chat, + params?: { onFinish?: () => void } +) { if (message instanceof AssistantMessage) { return renderAssistantMessage(message); } else if (message instanceof UserMessage) { - return renderUserMessage(message, chat); + return renderUserMessage(message, chat, params); } else if (message instanceof ErrorMessage) { return renderErrorMessage(message); } @@ -257,7 +299,11 @@ function renderAssistantMessage(message: AssistantMessage) { ); } -function renderUserMessage(message: UserMessage, chat: Chat) { +function renderUserMessage( + message: UserMessage, + chat: Chat, + params?: { onFinish?: () => void } +) { return (
@@ -289,7 +335,7 @@ function renderUserMessage(message: UserMessage, chat: Chat) { text size="tiny" class="mr-2" - onClick={() => chat.resendMessage(message.id)} + onClick={() => chat.resendMessage(message.id, params)} > resend diff --git a/gui/web/src/hooks/scroll.ts b/gui/web/src/hooks/scroll.ts new file mode 100644 index 0000000..065f6db --- /dev/null +++ b/gui/web/src/hooks/scroll.ts @@ -0,0 +1,69 @@ +import { isRef, Ref, watch } from "vue"; + +export function useAutoScroll(el: HTMLElement | Ref) { + const interval = 20; + let autoMode = true; + let timer: NodeJS.Timer; + let ele!: HTMLElement; + if (isRef(el)) { + let unwatch = watch( + el, + (e) => { + if (e) { + ele = e; + console.log('a', e) + ele.addEventListener("scroll", handleScroll); + unwatch(); + } + }, + { + immediate: true, + } + ); + } else { + ele = el; + ele.addEventListener("scroll", handleScroll); + } + + function resetAutoMode() { + if (ele.scrollTop === ele.scrollHeight - ele.clientHeight) { + autoMode = true; + } else { + autoMode = false; + } + } + + function handleScroll() { + resetAutoMode(); + } + + function start() { + autoMode = true + scrollToBottom(); + timer = setInterval(() => { + if (autoMode) { + scrollToBottom(); + } + }, interval); + } + + function stop() { + clearTimeout(timer); + } + + function destroy() { + stop(); + ele.removeEventListener("scroll", handleScroll); + } + + function scrollToBottom() { + ele.scrollTop = ele.scrollHeight - ele.clientHeight; + } + + return { + start, + stop, + destroy, + scrollToBottom, + }; +} diff --git a/gui/web/src/models/chat.ts b/gui/web/src/models/chat.ts index edfb60b..5e6b52d 100644 --- a/gui/web/src/models/chat.ts +++ b/gui/web/src/models/chat.ts @@ -17,19 +17,19 @@ export class Chat { this.messages = reactive(messages); } - async sendMessage(message: string) { + async sendMessage(message: string, params?: { onFinish?: () => void }) { const userMessage = reactive(new UserMessage(message)); this.messages.push(userMessage); const messageId = await sendMessage(this.id, message); userMessage.setId(messageId); - this.__receiveAssistantMessage(userMessage); + this.__receiveAssistantMessage(userMessage, params); return messageId; } - async resendMessage(messageId: string) { + async resendMessage(messageId: string, params?: { onFinish?: () => void }) { const index = this.messages.findIndex((item) => { return item instanceof UserMessage && item.id === messageId; }); @@ -41,10 +41,15 @@ export class Chat { await resendMessage(this.id, userMessage.id); - this.__receiveAssistantMessage(userMessage); + this.__receiveAssistantMessage(userMessage, params); } - async __receiveAssistantMessage(userMessage: UserMessage) { + async __receiveAssistantMessage( + userMessage: UserMessage, + params?: { + onFinish?: () => void; + } + ) { const userMessageId = userMessage.id; const { message: assistantMessage, @@ -66,6 +71,7 @@ export class Chat { this.messages.push(new ErrorMessage(chunk.data)); userMessage.delivered = false; this.busy = false; + params?.onFinish?.(); break; } case "data": { @@ -76,6 +82,7 @@ export class Chat { case "done": { assistantMessage.markHistory(); this.busy = false; + params?.onFinish?.(); unListen(); break; }