Skip to content

Commit

Permalink
Remove WaveAI dynamic height adjustment, use pure CSS, also fix scrol…
Browse files Browse the repository at this point in the history
…ling (#1594)

This makes the chat window flex-grow so we no longer need to manually
fix its height. It also cleans up some other styling.

It also fixes the scroll handlers so we detect when the user is at the
bottom of the chat window so we can follow the latest message. It also
fixes some circular references in the callbacks that were causing React
to bug out.
  • Loading branch information
esimkowitz authored Dec 20, 2024
1 parent 87309a0 commit e0c875a
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 133 deletions.
134 changes: 67 additions & 67 deletions frontend/app/view/waveai/waveai.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,107 +4,107 @@
.waveai {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
width: 100%;

.waveai-chat {
flex-grow: 1;
> .scrollable {
flex-flow: column nowrap;
margin-bottom: 0;
flex: 1 1 auto;
overflow: hidden;
.chat-window-container {
overflow-y: auto;
min-height: 100%;
margin-bottom: 0;
height: 100%;

.chat-window {
flex-flow: column nowrap;
display: flex;
flex-direction: column;
gap: 8px;

// This is the filler that will push the chat messages to the bottom until the chat window is full
.filler {
flex: 1 1 auto;
}
}

.chat-msg-container {
display: flex;
gap: 8px;
.chat-msg {
margin: 10px 0;
.chat-msg-container {
display: flex;
align-items: flex-start;
border-radius: 8px;

&.chat-msg-header {
gap: 8px;
.chat-msg {
margin: 10px 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
border-radius: 8px;

.icon-box {
padding-top: 0;
border-radius: 4px;
background-color: rgb(from var(--highlight-bg-color) r g b / 0.05);
&.chat-msg-header {
display: flex;
padding: 6px;
flex-direction: column;
justify-content: flex-start;

.icon-box {
padding-top: 0;
border-radius: 4px;
background-color: rgb(from var(--highlight-bg-color) r g b / 0.05);
display: flex;
padding: 6px;
}
}
}

&.chat-msg-assistant {
color: var(--main-text-color);
background-color: rgb(from var(--highlight-bg-color) r g b / 0.1);
margin-right: auto;
padding: 10px;
max-width: 85%;

.markdown {
width: 100%;

pre {
white-space: pre-wrap;
word-break: break-word;
max-width: 100%;
overflow-x: auto;
margin-left: 0;
&.chat-msg-assistant {
color: var(--main-text-color);
background-color: rgb(from var(--highlight-bg-color) r g b / 0.1);
margin-right: auto;
padding: 10px;
max-width: 85%;

.markdown {
width: 100%;

pre {
white-space: pre-wrap;
word-break: break-word;
max-width: 100%;
overflow-x: auto;
margin-left: 0;
}
}
}
}
&.chat-msg-user {
margin-left: auto;
padding: 10px;
max-width: 85%;
background-color: rgb(from var(--accent-color) r g b / 0.15);
}
&.chat-msg-user {
margin-left: auto;
padding: 10px;
max-width: 85%;
background-color: rgb(from var(--accent-color) r g b / 0.15);
}

&.chat-msg-error {
color: var(--main-text-color);
background-color: rgb(from var(--error-color) r g b / 0.25);
margin-right: auto;
padding: 10px;
max-width: 85%;

.markdown {
width: 100%;

pre {
white-space: pre-wrap;
word-break: break-word;
max-width: 100%;
overflow-x: auto;
margin-left: 0;
&.chat-msg-error {
color: var(--main-text-color);
background-color: rgb(from var(--error-color) r g b / 0.25);
margin-right: auto;
padding: 10px;
max-width: 85%;

.markdown {
width: 100%;

pre {
white-space: pre-wrap;
word-break: break-word;
max-width: 100%;
overflow-x: auto;
margin-left: 0;
}
}
}
}

&.typing-indicator {
margin-top: 4px;
&.typing-indicator {
margin-top: 4px;
}
}
}
}
}
}

.waveai-controls {
flex: 0 0 auto;
display: flex;
flex-direction: row;
align-items: center;
Expand Down
144 changes: 78 additions & 66 deletions frontend/app/view/waveai/waveai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { Button } from "@/app/element/button";
import { Markdown } from "@/app/element/markdown";
import { TypingIndicator } from "@/app/element/typingindicator";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
Expand All @@ -17,6 +16,7 @@ import { atom, Atom, PrimitiveAtom, useAtomValue, WritableAtom } from "jotai";
import type { OverlayScrollbars } from "overlayscrollbars";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
import { debounce, throttle } from "throttle-debounce";
import "./waveai.scss";

interface ChatMessageType {
Expand Down Expand Up @@ -434,8 +434,6 @@ function makeWaveAiViewModel(blockId: string): WaveAiModel {

const ChatItem = ({ chatItem, model }: ChatItemProps) => {
const { user, text } = chatItem;
const cssVar = "--panel-bg-color";
const panelBgColor = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim();
const fontSize = useOverrideConfigAtom(model.blockId, "ai:fontsize");
const fixedFontSize = useOverrideConfigAtom(model.blockId, "ai:fixedfontsize");
const renderContent = useMemo(() => {
Expand Down Expand Up @@ -507,25 +505,23 @@ interface ChatWindowProps {
messages: ChatMessageType[];
msgWidths: Object;
model: WaveAiModel;
height: number;
}

const ChatWindow = memo(
forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(
({ chatWindowRef, messages, msgWidths, model, height }, ref) => {
const [isUserScrolling, setIsUserScrolling] = useState(false);
forwardRef<OverlayScrollbarsComponentRef, ChatWindowProps>(({ chatWindowRef, messages, msgWidths, model }, ref) => {
const [isUserScrolling, setIsUserScrolling] = useState(false);

const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const prevMessagesLenRef = useRef(messages.length);
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const prevMessagesLenRef = useRef(messages.length);

useImperativeHandle(ref, () => osRef.current as OverlayScrollbarsComponentRef);
useImperativeHandle(ref, () => osRef.current as OverlayScrollbarsComponentRef);

useEffect(() => {
if (osRef.current && osRef.current.osInstance()) {
const handleNewMessage = useCallback(
throttle(100, (messages: ChatMessageType[]) => {
if (osRef.current?.osInstance()) {
const { viewport } = osRef.current.osInstance().elements();
const curMessagesLen = messages.length;
if (prevMessagesLenRef.current !== curMessagesLen || !isUserScrolling) {
setIsUserScrolling(false);
viewport.scrollTo({
behavior: "auto",
top: chatWindowRef.current?.scrollHeight || 0,
Expand All @@ -534,61 +530,81 @@ const ChatWindow = memo(

prevMessagesLenRef.current = curMessagesLen;
}
}, [messages, isUserScrolling]);
}),
[isUserScrolling]
);

useEffect(() => {
if (osRef.current && osRef.current.osInstance()) {
const { viewport } = osRef.current.osInstance().elements();
useEffect(() => {
handleNewMessage(messages);
}, [messages]);

// Wait 300 ms after the user stops scrolling to determine if the user is within 300px of the bottom of the chat window.
// If so, unset the user scrolling flag.
const determineUnsetScroll = useCallback(
debounce(300, () => {
const { viewport } = osRef.current.osInstance().elements();
if (viewport.scrollTop > chatWindowRef.current?.clientHeight - viewport.clientHeight - 30) {
setIsUserScrolling(false);
}
}),
[]
);

const handleUserScroll = () => {
setIsUserScrolling(true);
};
const handleUserScroll = useCallback(
throttle(100, () => {
setIsUserScrolling(true);
determineUnsetScroll();
}),
[]
);

viewport.addEventListener("wheel", handleUserScroll, { passive: true });
viewport.addEventListener("touchmove", handleUserScroll, { passive: true });
useEffect(() => {
if (osRef.current?.osInstance()) {
const { viewport } = osRef.current.osInstance().elements();

return () => {
viewport.removeEventListener("wheel", handleUserScroll);
viewport.removeEventListener("touchmove", handleUserScroll);
if (osRef.current && osRef.current.osInstance()) {
osRef.current.osInstance().destroy();
}
};
}
}, []);

const handleScrollbarInitialized = (instance: OverlayScrollbars) => {
const { viewport } = instance.elements();
viewport.removeAttribute("tabindex");
viewport.scrollTo({
behavior: "auto",
top: chatWindowRef.current?.scrollHeight || 0,
});
};
viewport.addEventListener("wheel", handleUserScroll, { passive: true });
viewport.addEventListener("touchmove", handleUserScroll, { passive: true });

const handleScrollbarUpdated = (instance: OverlayScrollbars) => {
const { viewport } = instance.elements();
viewport.removeAttribute("tabindex");
};
return () => {
viewport.removeEventListener("wheel", handleUserScroll);
viewport.removeEventListener("touchmove", handleUserScroll);
if (osRef.current && osRef.current.osInstance()) {
osRef.current.osInstance().destroy();
}
};
}
}, []);

return (
<OverlayScrollbarsComponent
ref={osRef}
className="scrollable"
options={{ scrollbars: { autoHide: "leave" } }}
events={{ initialized: handleScrollbarInitialized, updated: handleScrollbarUpdated }}
style={{ maxHeight: height }}
>
<div ref={chatWindowRef} className="chat-window" style={msgWidths}>
<div className="filler"></div>
{messages.map((chitem, idx) => (
<ChatItem key={idx} chatItem={chitem} model={model} />
))}
</div>
</OverlayScrollbarsComponent>
);
}
)
const handleScrollbarInitialized = (instance: OverlayScrollbars) => {
const { viewport } = instance.elements();
viewport.removeAttribute("tabindex");
viewport.scrollTo({
behavior: "auto",
top: chatWindowRef.current?.scrollHeight || 0,
});
};

const handleScrollbarUpdated = (instance: OverlayScrollbars) => {
const { viewport } = instance.elements();
viewport.removeAttribute("tabindex");
};

return (
<OverlayScrollbarsComponent
ref={osRef}
className="chat-window-container"
options={{ scrollbars: { autoHide: "leave" } }}
events={{ initialized: handleScrollbarInitialized, updated: handleScrollbarUpdated }}
>
<div ref={chatWindowRef} className="chat-window" style={msgWidths}>
<div className="filler"></div>
{messages.map((chitem, idx) => (
<ChatItem key={idx} chatItem={chitem} model={model} />
))}
</div>
</OverlayScrollbarsComponent>
);
})
);

interface ChatInputProps {
Expand Down Expand Up @@ -662,8 +678,6 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
const chatWindowRef = useRef<HTMLDivElement>(null);
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const waveAiDims = useDimensionsWithExistingRef(waveaiRef);
const chatInputDims = useDimensionsWithExistingRef(inputRef);

const [value, setValue] = useState("");
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
Expand Down Expand Up @@ -836,8 +850,6 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
messages={messages}
msgWidths={msgWidths}
model={model}
height={waveAiDims?.height - chatInputDims?.height - 28 ?? 400}
// the 28 is a magic number it the moment but it makes the spacing look good
/>
</div>
<div className="waveai-controls">
Expand Down

0 comments on commit e0c875a

Please sign in to comment.