diff --git a/frontend/src/scenes/max/Thread.tsx b/frontend/src/scenes/max/Thread.tsx index de3a9ee5b75f4..5014d8a752509 100644 --- a/frontend/src/scenes/max/Thread.tsx +++ b/frontend/src/scenes/max/Thread.tsx @@ -15,9 +15,10 @@ import { TopHeading } from 'lib/components/Cards/InsightCard/TopHeading' import { IconOpenInNew } from 'lib/lemon-ui/icons' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' import posthog from 'posthog-js' -import React, { useMemo, useRef, useState } from 'react' +import React, { useMemo, useState } from 'react' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' +import { twMerge } from 'tailwind-merge' import { Query } from '~/queries/Query/Query' import { @@ -75,14 +76,19 @@ function MessageGroup({ messages, isFinal: isGroupFinal, index: messageGroupInde className="mt-1 border" /> -
+
{messages.map((message, messageIndex) => { if (isHumanMessage(message)) { return ( {message.content || '*No text.*'} @@ -120,7 +126,7 @@ function MessageGroup({ messages, isFinal: isGroupFinal, index: messageGroupInde return null // We currently skip other types of messages })} {messages.at(-1)?.status === 'error' && ( - +
Max is generating this answer one more time because the previous attempt has failed. @@ -132,17 +138,28 @@ function MessageGroup({ messages, isFinal: isGroupFinal, index: messageGroupInde ) } -const MessageTemplate = React.forwardRef< - HTMLDivElement, - { type: 'human' | 'ai'; className?: string; action?: React.ReactNode; children: React.ReactNode } ->(function MessageTemplate({ type, children, className, action }, ref) { +interface MessageTemplateProps { + type: 'human' | 'ai' + action?: React.ReactNode + className?: string + boxClassName?: string + children: React.ReactNode +} + +const MessageTemplate = React.forwardRef(function MessageTemplate( + { type, children, className, boxClassName, action }, + ref +) { return ( -
+
{children} @@ -166,7 +183,7 @@ const TextAnswer = React.forwardRef(function Te return ( - -
+ +
@@ -264,7 +281,6 @@ function SuccessActions({ const [rating, setRating] = useState<'good' | 'bad' | null>(null) const [feedback, setFeedback] = useState('') const [feedbackInputStatus, setFeedbackInputStatus] = useState<'hidden' | 'pending' | 'submitted'>('hidden') - const hasScrolledFeedbackInputIntoView = useRef(false) const [relevantHumanMessage, relevantVisualizationMessage] = useMemo(() => { // We need to find the relevant visualization message (which might be a message earlier if the most recent one @@ -335,16 +351,7 @@ function SuccessActions({ )}
{feedbackInputStatus !== 'hidden' && ( - { - if (el && !hasScrolledFeedbackInputIntoView.current) { - // When the feedback input is first rendered, scroll it into view - el.scrollIntoView({ behavior: 'smooth' }) - hasScrolledFeedbackInputIntoView.current = true - } - }} - > +

{feedbackInputStatus === 'pending' diff --git a/frontend/src/scenes/max/maxLogic.ts b/frontend/src/scenes/max/maxLogic.ts index d143bdfd72db1..aa8c66e7f9e40 100644 --- a/frontend/src/scenes/max/maxLogic.ts +++ b/frontend/src/scenes/max/maxLogic.ts @@ -1,22 +1,10 @@ import { captureException } from '@sentry/react' import { shuffle } from 'd3' import { createParser } from 'eventsource-parser' -import { - actions, - afterMount, - connect, - kea, - key, - listeners, - path, - props, - reducers, - selectors, - sharedListeners, -} from 'kea' +import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api, { ApiError } from 'lib/api' -import { isHumanMessage } from 'scenes/max/utils' +import { isHumanMessage, isVisualizationMessage } from 'scenes/max/utils' import { projectLogic } from 'scenes/projectLogic' import { @@ -69,6 +57,7 @@ export const maxLogic = kea([ setVisibleSuggestions: (suggestions: string[]) => ({ suggestions }), shuffleVisibleSuggestions: true, retryLastMessage: true, + scrollThreadToBottom: true, }), reducers({ question: [ @@ -128,18 +117,7 @@ export const maxLogic = kea([ }, ], }), - sharedListeners({ - scrollThreadToBottom: () => { - requestAnimationFrame(() => { - // On next frame so that the message has been rendered - const mainEl = document.querySelector('main') - if (mainEl) { - mainEl.scrollTop = mainEl.scrollHeight - } - }) - }, - }), - listeners(({ actions, values, sharedListeners, props }) => ({ + listeners(({ actions, values, props }) => ({ [projectLogic.actionTypes.updateCurrentProjectSuccess]: ({ payload }) => { // Load suggestions anew after product description is changed on the project // Most important when description is set for the first time, but also when updated, @@ -253,8 +231,26 @@ export const maxLogic = kea([ actions.askMax(lastMessage.content) } }, - addMessage: sharedListeners.scrollThreadToBottom, - replaceMessage: sharedListeners.scrollThreadToBottom, + addMessage: (payload) => { + if (isHumanMessage(payload.message) || isVisualizationMessage(payload.message)) { + actions.scrollThreadToBottom() + } + }, + replaceMessage: (payload) => { + if (isVisualizationMessage(payload.message)) { + actions.scrollThreadToBottom() + } + }, + scrollThreadToBottom: () => { + requestAnimationFrame(() => { + // On next frame so that the message has been rendered + const mainEl = document.querySelector('main') + mainEl?.scrollTo({ + top: mainEl?.scrollHeight, + behavior: 'smooth', + }) + }) + }, })), selectors({ sessionId: [(_, p) => [p.sessionId], (sessionId) => sessionId],