Skip to content

Commit

Permalink
fix(product-assistant): UI/UX improvements for visualization messages (
Browse files Browse the repository at this point in the history
…#26434)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Matloka <[email protected]>
  • Loading branch information
3 people authored Nov 27, 2024
1 parent 3305554 commit 0b9d5f7
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 53 deletions.
57 changes: 32 additions & 25 deletions frontend/src/scenes/max/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -75,14 +76,19 @@ function MessageGroup({ messages, isFinal: isGroupFinal, index: messageGroupInde
className="mt-1 border"
/>
</Tooltip>
<div className="flex flex-col gap-2 min-w-0">
<div
className={clsx(
'flex flex-col gap-2 min-w-0 w-full',
groupType === 'human' ? 'items-end' : 'items-start'
)}
>
{messages.map((message, messageIndex) => {
if (isHumanMessage(message)) {
return (
<MessageTemplate
key={messageIndex}
type="human"
className={message.status === 'error' ? 'border-danger' : undefined}
boxClassName={message.status === 'error' ? 'border-danger' : undefined}
>
<LemonMarkdown>{message.content || '*No text.*'}</LemonMarkdown>
</MessageTemplate>
Expand Down Expand Up @@ -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' && (
<MessageTemplate type="ai" className="border-warning">
<MessageTemplate type="ai" boxClassName="border-warning">
<div className="flex items-center gap-1.5">
<IconWarning className="text-xl text-warning" />
<i>Max is generating this answer one more time because the previous attempt has failed.</i>
Expand All @@ -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<HTMLDivElement, MessageTemplateProps>(function MessageTemplate(
{ type, children, className, boxClassName, action },
ref
) {
return (
<div className={clsx('flex flex-col gap-1', type === 'human' ? 'items-end' : 'items-start')} ref={ref}>
<div
className={twMerge('flex flex-col gap-1 w-full', type === 'human' ? 'items-end' : 'items-start', className)}
ref={ref}
>
<div
className={clsx(
className={twMerge(
'border py-2 px-3 rounded-lg bg-bg-light',
type === 'human' && 'font-medium',
className
boxClassName
)}
>
{children}
Expand All @@ -166,7 +183,7 @@ const TextAnswer = React.forwardRef<HTMLDivElement, TextAnswerProps>(function Te
return (
<MessageTemplate
type="ai"
className={message.status === 'error' || message.type === 'ai/failure' ? 'border-danger' : undefined}
boxClassName={message.status === 'error' || message.type === 'ai/failure' ? 'border-danger' : undefined}
ref={ref}
action={
message.status === 'completed' &&
Expand Down Expand Up @@ -210,8 +227,8 @@ function VisualizationAnswer({
? null
: query && (
<>
<MessageTemplate type="ai">
<div className="h-96 flex">
<MessageTemplate type="ai" className="w-full" boxClassName="w-full">
<div className="min-h-80 flex">
<Query query={query} readOnly embedded />
</div>
<div className="relative mb-1">
Expand Down Expand Up @@ -264,7 +281,6 @@ function SuccessActions({
const [rating, setRating] = useState<'good' | 'bad' | null>(null)
const [feedback, setFeedback] = useState<string>('')
const [feedbackInputStatus, setFeedbackInputStatus] = useState<'hidden' | 'pending' | 'submitted'>('hidden')
const hasScrolledFeedbackInputIntoView = useRef<boolean>(false)

const [relevantHumanMessage, relevantVisualizationMessage] = useMemo(() => {
// We need to find the relevant visualization message (which might be a message earlier if the most recent one
Expand Down Expand Up @@ -335,16 +351,7 @@ function SuccessActions({
)}
</div>
{feedbackInputStatus !== 'hidden' && (
<MessageTemplate
type="ai"
ref={(el) => {
if (el && !hasScrolledFeedbackInputIntoView.current) {
// When the feedback input is first rendered, scroll it into view
el.scrollIntoView({ behavior: 'smooth' })
hasScrolledFeedbackInputIntoView.current = true
}
}}
>
<MessageTemplate type="ai">
<div className="flex items-center">
<h4 className="m-0 text-sm grow">
{feedbackInputStatus === 'pending'
Expand Down
52 changes: 24 additions & 28 deletions frontend/src/scenes/max/maxLogic.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -69,6 +57,7 @@ export const maxLogic = kea<maxLogicType>([
setVisibleSuggestions: (suggestions: string[]) => ({ suggestions }),
shuffleVisibleSuggestions: true,
retryLastMessage: true,
scrollThreadToBottom: true,
}),
reducers({
question: [
Expand Down Expand Up @@ -128,18 +117,7 @@ export const maxLogic = kea<maxLogicType>([
},
],
}),
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,
Expand Down Expand Up @@ -253,8 +231,26 @@ export const maxLogic = kea<maxLogicType>([
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],
Expand Down

0 comments on commit 0b9d5f7

Please sign in to comment.