Skip to content

Commit

Permalink
Merge pull request #18 from lisiur/feat/auto-scroll
Browse files Browse the repository at this point in the history
feat: support auto scroll
  • Loading branch information
lisiur committed Mar 15, 2023
2 parents 5e88b78 + eedd81e commit f666ce1
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 13 deletions.
62 changes: 54 additions & 8 deletions gui/web/src/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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: {
Expand All @@ -27,9 +36,29 @@ export default defineComponent({
},
},
setup(props) {
const scrollRef = ref<InstanceType<typeof NScrollbar>>();
const inputRef = ref<HTMLTextAreaElement>();
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();
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -105,10 +139,14 @@ export default defineComponent({
style="background-color: var(--body-color)"
>
<div class="flex-1 overflow-hidden py-4">
<NScrollbar>
<NScrollbar ref={scrollRef}>
<div class="grid gap-6">
{props.chat.messages.map((message, index) => (
<div key={index}>{renderMessage(message, props.chat)}</div>
<div key={index}>
{renderMessage(message, props.chat, {
onFinish: stopAutoScroll,
})}
</div>
))}
</div>
</NScrollbar>
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -257,7 +299,11 @@ function renderAssistantMessage(message: AssistantMessage) {
);
}

function renderUserMessage(message: UserMessage, chat: Chat) {
function renderUserMessage(
message: UserMessage,
chat: Chat,
params?: { onFinish?: () => void }
) {
return (
<div class="flex justify-end items-start pr-4 pl-16">
<div class="relative mr-2">
Expand Down Expand Up @@ -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
</NButton>
Expand Down
69 changes: 69 additions & 0 deletions gui/web/src/hooks/scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { isRef, Ref, watch } from "vue";

export function useAutoScroll(el: HTMLElement | Ref<HTMLElement>) {
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,
};
}
17 changes: 12 additions & 5 deletions gui/web/src/models/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand All @@ -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,
Expand All @@ -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": {
Expand All @@ -76,6 +82,7 @@ export class Chat {
case "done": {
assistantMessage.markHistory();
this.busy = false;
params?.onFinish?.();
unListen();
break;
}
Expand Down

0 comments on commit f666ce1

Please sign in to comment.