From e0c875afebca450dfbb881ba766721443f57fa86 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 20 Dec 2024 11:49:29 -0800 Subject: [PATCH 1/2] Remove WaveAI dynamic height adjustment, use pure CSS, also fix scrolling (#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. --- frontend/app/view/waveai/waveai.scss | 132 ++++++++++++------------ frontend/app/view/waveai/waveai.tsx | 144 +++++++++++++++------------ 2 files changed, 144 insertions(+), 132 deletions(-) diff --git a/frontend/app/view/waveai/waveai.scss b/frontend/app/view/waveai/waveai.scss index 4a2020e5c..2d463fd88 100644 --- a/frontend/app/view/waveai/waveai.scss +++ b/frontend/app/view/waveai/waveai.scss @@ -4,100 +4,99 @@ .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; - &.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; + .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-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%; + &.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%; + .markdown { + width: 100%; - pre { - white-space: pre-wrap; - word-break: break-word; - max-width: 100%; - overflow-x: auto; - margin-left: 0; + 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); + } - &.typing-indicator { - margin-top: 4px; + &.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; + } } } } @@ -105,6 +104,7 @@ } .waveai-controls { + flex: 0 0 auto; display: flex; flex-direction: row; align-items: center; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 63434914a..e3c131759 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -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"; @@ -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 { @@ -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(() => { @@ -507,25 +505,23 @@ interface ChatWindowProps { messages: ChatMessageType[]; msgWidths: Object; model: WaveAiModel; - height: number; } const ChatWindow = memo( - forwardRef( - ({ chatWindowRef, messages, msgWidths, model, height }, ref) => { - const [isUserScrolling, setIsUserScrolling] = useState(false); + forwardRef(({ chatWindowRef, messages, msgWidths, model }, ref) => { + const [isUserScrolling, setIsUserScrolling] = useState(false); - const osRef = useRef(null); - const prevMessagesLenRef = useRef(messages.length); + const osRef = useRef(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, @@ -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]); - const handleUserScroll = () => { - setIsUserScrolling(true); - }; - - viewport.addEventListener("wheel", handleUserScroll, { passive: true }); - viewport.addEventListener("touchmove", handleUserScroll, { passive: true }); - - return () => { - viewport.removeEventListener("wheel", handleUserScroll); - viewport.removeEventListener("touchmove", handleUserScroll); - if (osRef.current && osRef.current.osInstance()) { - osRef.current.osInstance().destroy(); - } - }; + // 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 handleScrollbarInitialized = (instance: OverlayScrollbars) => { - const { viewport } = instance.elements(); - viewport.removeAttribute("tabindex"); - viewport.scrollTo({ - behavior: "auto", - top: chatWindowRef.current?.scrollHeight || 0, - }); - }; + const handleUserScroll = useCallback( + throttle(100, () => { + setIsUserScrolling(true); + determineUnsetScroll(); + }), + [] + ); - const handleScrollbarUpdated = (instance: OverlayScrollbars) => { - const { viewport } = instance.elements(); - viewport.removeAttribute("tabindex"); - }; + useEffect(() => { + if (osRef.current?.osInstance()) { + const { viewport } = osRef.current.osInstance().elements(); - return ( - -
-
- {messages.map((chitem, idx) => ( - - ))} -
-
- ); - } - ) + viewport.addEventListener("wheel", handleUserScroll, { passive: true }); + viewport.addEventListener("touchmove", handleUserScroll, { passive: true }); + + 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, + }); + }; + + const handleScrollbarUpdated = (instance: OverlayScrollbars) => { + const { viewport } = instance.elements(); + viewport.removeAttribute("tabindex"); + }; + + return ( + +
+
+ {messages.map((chitem, idx) => ( + + ))} +
+
+ ); + }) ); interface ChatInputProps { @@ -662,8 +678,6 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { const chatWindowRef = useRef(null); const osRef = useRef(null); const inputRef = useRef(null); - const waveAiDims = useDimensionsWithExistingRef(waveaiRef); - const chatInputDims = useDimensionsWithExistingRef(inputRef); const [value, setValue] = useState(""); const [selectedBlockIdx, setSelectedBlockIdx] = useState(null); @@ -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 />
From 9793f9898cd97d2d5f0ec916a4315aab0a409ab6 Mon Sep 17 00:00:00 2001 From: "wave-builder[bot]" <181805596+wave-builder[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:57:30 +0000 Subject: [PATCH 2/2] chore: bump package version to 0.10.4-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91b39c14e..eb34744c4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "productName": "Wave", "description": "Open-Source AI-Native Terminal Built for Seamless Workflows", "license": "Apache-2.0", - "version": "0.10.4-beta.0", + "version": "0.10.4-beta.1", "homepage": "https://waveterm.dev", "build": { "appId": "dev.commandline.waveterm"