Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4abd345
feat: hardcoded suggestions
skoob13 May 7, 2025
1a7b05d
feat: animation
skoob13 May 9, 2025
22358c6
feat: render elements & focus of elements
skoob13 May 21, 2025
479c865
feat: animations
skoob13 May 22, 2025
f1e27f7
feat: better copies
skoob13 May 22, 2025
12589f2
feat: prevent layout shift
skoob13 May 22, 2025
c95b2c8
fix: break all
skoob13 May 22, 2025
2c29bef
fix: logic
skoob13 May 22, 2025
59ed025
fix: copy
skoob13 May 22, 2025
bc44bce
feat: dropdown
skoob13 May 23, 2025
9e1fcc6
fix: handle clicks
skoob13 May 25, 2025
dad9c82
refactor: merge logics
skoob13 May 25, 2025
0ba670c
fix: types
skoob13 May 26, 2025
780157c
test: logic
skoob13 May 26, 2025
819c9cc
fix: better suggestion for the conversion rate
skoob13 May 26, 2025
81c73b6
Update UI snapshots for `chromium` (1)
github-actions[bot] May 22, 2025
306ed0b
Update UI snapshots for `chromium` (1)
github-actions[bot] May 26, 2025
0aa616c
Merge branch 'master' into feat/max-suggestions
Twixes May 26, 2025
2ec705b
Merge branch 'master' of github.com:PostHog/posthog into feat/max-sug…
skoob13 May 27, 2025
3d6716b
fix: review comments
skoob13 May 27, 2025
416fa3f
feat: flexible suggestions
skoob13 May 27, 2025
1bdcd4a
fix: strip the previous iteration of suggestions
skoob13 May 27, 2025
a487c15
feat: exit animation
skoob13 May 27, 2025
be99bcc
Merge branch 'feat/max-suggestions' of github.com:PostHog/posthog int…
skoob13 May 27, 2025
7904c0d
Update UI snapshots for `chromium` (1)
github-actions[bot] May 27, 2025
9772583
Remove now-redundant "Ask me about your product ..."
Twixes May 27, 2025
56ee5b9
Update UI snapshots for `chromium` (1)
github-actions[bot] May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/scenes-app-max-ai--welcome--dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/scenes-app-max-ai--welcome--light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@
"@posthog/tailwind": "workspace:*",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dismissable-layer": "^1.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.10",
"@react-hook/size": "^2.1.2",
"@stripe/react-stripe-js": "^2.8.0",
"@stripe/stripe-js": "^4.5.0",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/data-warehouse/editor/QueryPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function QueryPane(props: QueryPaneProps): JSX.Element {
}}
introOverride={{
headline: 'What data do you want to analyze?',
description: 'On this page, I can help you write and tweak SQL queries.',
description: 'Let me help you quickly write SQL, and tweak it.',
}}
>
<div className="relative" />
Expand Down
85 changes: 36 additions & 49 deletions frontend/src/scenes/max/Max.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Meta, StoryFn } from '@storybook/react'
import { useActions, useValues } from 'kea'
import { MOCK_DEFAULT_ORGANIZATION } from 'lib/api.mock'
import { useEffect } from 'react'
import { maxSettingsLogic } from 'scenes/settings/environment/maxSettingsLogic'
import { twMerge } from 'tailwind-merge'

import { mswDecorator, useStorybookMocks } from '~/mocks/browser'
Expand All @@ -17,7 +16,7 @@ import {
} from './__mocks__/chatResponse.mocks'
import conversationList from './__mocks__/conversationList.json'
import { MaxInstance, MaxInstanceProps } from './Max'
import { maxLogic } from './maxLogic'
import { maxLogic, QUESTION_SUGGESTIONS_DATA } from './maxLogic'
import { maxThreadLogic } from './maxThreadLogic'

const meta: Meta = {
Expand Down Expand Up @@ -72,53 +71,6 @@ export const Welcome: StoryFn = () => {
return <Template />
}

export const WelcomeSuggestionsAvailable: StoryFn = () => {
useStorybookMocks({
post: {
'/api/environments/:team_id/query/': () => [
200,
{
questions: [
'What are our most popular pages in the blog?',
'Where are our new users located?',
'Who are the biggest customers using our paid product?',
'Which feature drives most usage?',
],
},
],
},
})

const { loadCoreMemorySuccess } = useActions(maxSettingsLogic)

useEffect(() => {
loadCoreMemorySuccess({ id: 'x', text: 'A Storybook test.' })
}, [])

return <Template />
}

export const WelcomeLoadingSuggestions: StoryFn = () => {
useStorybookMocks({
post: {
'/api/environments/:team_id/query/': (_req, _res, ctx) => [ctx.delay('infinite')],
},
})

const { loadCoreMemorySuccess } = useActions(maxSettingsLogic)

useEffect(() => {
loadCoreMemorySuccess({ id: 'x', text: 'A Storybook test.' })
}, [])

return <Template />
}
WelcomeLoadingSuggestions.parameters = {
testOptions: {
waitForLoadersToDisappear: false,
},
}

export const WelcomeFeaturePreviewAutoEnrolled: StoryFn = () => {
return <Template />
}
Expand Down Expand Up @@ -380,3 +332,38 @@ ChatHistoryLoading.parameters = {
waitForLoadersToDisappear: false,
},
}

export const ThreadWithOpenedSuggestionsMobile: StoryFn = () => {
const { setActiveGroup } = useActions(maxLogic)

useEffect(() => {
// The largest group is the set up group
setActiveGroup(QUESTION_SUGGESTIONS_DATA[3])
}, [])

return <Template sidePanel />
}
ThreadWithOpenedSuggestionsMobile.parameters = {
testOptions: {
waitForLoadersToDisappear: false,
},
viewport: {
defaultViewport: 'mobile2',
},
}

export const ThreadWithOpenedSuggestions: StoryFn = () => {
const { setActiveGroup } = useActions(maxLogic)

useEffect(() => {
// The largest group is the set up group
setActiveGroup(QUESTION_SUGGESTIONS_DATA[3])
}, [])

return <Template sidePanel />
}
ThreadWithOpenedSuggestions.parameters = {
testOptions: {
waitForLoadersToDisappear: false,
},
}
5 changes: 2 additions & 3 deletions frontend/src/scenes/max/Max.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { maxGlobalLogic } from './maxGlobalLogic'
import { maxLogic } from './maxLogic'
import { maxThreadLogic, MaxThreadLogicProps } from './maxThreadLogic'
import { QuestionInput } from './QuestionInput'
import { QuestionSuggestions } from './QuestionSuggestions'
import { QuestionInputWithSuggestions } from './QuestionInputWithSuggestions'
import { Thread } from './Thread'

export const scene: SceneExport = {
Expand Down Expand Up @@ -207,8 +207,7 @@ export const MaxInstance = React.memo(function MaxInstance({ sidePanel }: MaxIns
)}
<div className="flex-1 items-center justify-center flex flex-col gap-3">
<Intro />
<QuestionInput />
<QuestionSuggestions />
<QuestionInputWithSuggestions />
</div>
<HistoryPreview sidePanel={sidePanel} />
</div>
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/scenes/max/QuestionInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.QuestionInput__SuggestionsList {
&.QuestionInput__SuggestionsList-enter {
opacity: 0;
transform: translateY(-0.5rem);
}

&.QuestionInput__SuggestionsList-enter-active {
opacity: 1;
transition: opacity 150ms ease-out, transform 150ms ease-out;
transform: translateY(0);
}

&.QuestionInput__SuggestionsList-exit {
opacity: 1;
transform: scale(1) translateY(0);
}

&.QuestionInput__SuggestionsList-exit-active {
opacity: 0;
transition: opacity 150ms ease-out, transform 150ms ease-out;
transform: scale(0.95) translateY(-0.5rem);
}
}

.QuestionInput__QuestionSuggestion {
opacity: 0;
animation: QuestionInput__QuestionSuggestion__slide-in-fade-down 60ms ease-out forwards;
animation-delay: calc(var(--index) * 25ms);
}

@keyframes QuestionInput__QuestionSuggestion__slide-in-fade-down {
from {
opacity: 0;
transform: translateY(-0.5rem);
}

to {
opacity: 1;
transform: translateY(0);
}
}
87 changes: 83 additions & 4 deletions frontend/src/scenes/max/QuestionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import './QuestionInput.scss'

import { IconArrowRight, IconStopFilled } from '@posthog/icons'
import { LemonButton, LemonTextArea } from '@posthog/lemon-ui'
import { ToggleGroup, ToggleGroupItem } from '@radix-ui/react-toggle-group'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { IconTools } from 'lib/lemon-ui/icons'
import { useEffect, useRef } from 'react'
import { CSSTransition } from 'react-transition-group'

import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'

import { maxGlobalLogic } from './maxGlobalLogic'
import { maxLogic } from './maxLogic'
import { maxLogic, SuggestionGroup } from './maxLogic'
import { maxThreadLogic } from './maxThreadLogic'
import { checkSuggestionRequiresUserInput, formatSuggestion, stripSuggestionPlaceholders } from './utils'

interface QuestionInputProps {
isFloating?: boolean
Expand All @@ -18,7 +23,7 @@ interface QuestionInputProps {
export function QuestionInput({ isFloating }: QuestionInputProps): JSX.Element {
const { tools } = useValues(maxGlobalLogic)

const { question } = useValues(maxLogic)
const { question, focusCounter } = useValues(maxLogic)
const { setQuestion } = useActions(maxLogic)

const { threadLoading, inputDisabled, submissionDisabledReason } = useValues(maxThreadLogic)
Expand All @@ -34,11 +39,10 @@ export function QuestionInput({ isFloating }: QuestionInputProps): JSX.Element {

useEffect(() => {
if (textAreaRef.current) {
// Autofocus, but a version that also moves cursor to end of text
textAreaRef.current.focus()
textAreaRef.current.setSelectionRange(textAreaRef.current.value.length, textAreaRef.current.value.length)
}
}, [])
}, [focusCounter]) // Update focus when focusCounter changes

return (
<div
Expand Down Expand Up @@ -95,6 +99,7 @@ export function QuestionInput({ isFloating }: QuestionInputProps): JSX.Element {
icon={threadLoading ? <IconStopFilled /> : <IconArrowRight />}
/>
</div>
{!isFloating && <SuggestionsList />}
</div>
{tools.length > 0 && (
<div
Expand All @@ -118,3 +123,77 @@ export function QuestionInput({ isFloating }: QuestionInputProps): JSX.Element {
</div>
)
}

function SuggestionsList(): JSX.Element {
const focusElementRef = useRef<HTMLDivElement | null>(null)
const previousSuggestionGroup = useRef<SuggestionGroup | null>(null)

const { setQuestion, focusInput, setActiveGroup } = useActions(maxLogic)
const { activeSuggestionGroup } = useValues(maxLogic)
const { askMax } = useActions(maxThreadLogic)

useEffect(() => {
if (focusElementRef.current && activeSuggestionGroup) {
focusElementRef.current.focus()
}
previousSuggestionGroup.current = activeSuggestionGroup
}, [activeSuggestionGroup])

const memoizedSuggestion = activeSuggestionGroup || previousSuggestionGroup.current

return (
<CSSTransition
in={!!activeSuggestionGroup}
timeout={150}
classNames="QuestionInput__SuggestionsList"
mountOnEnter
unmountOnExit
nodeRef={focusElementRef}
>
<ToggleGroup
ref={focusElementRef}
type="single"
className="QuestionInput__SuggestionsList absolute inset-x-2 top-full grid auto-rows-auto p-1 border-x border-b rounded-b-lg backdrop-blur-sm bg-[var(--glass-bg-3000)] z-10"
onValueChange={(index) => {
const suggestion = activeSuggestionGroup?.suggestions[Number(index)]
if (!suggestion) {
return
}

if (checkSuggestionRequiresUserInput(suggestion.content)) {
// Content requires to write something to continue
setQuestion(stripSuggestionPlaceholders(suggestion.content))
focusInput()
} else {
// Otherwise, just launch the generation
askMax(suggestion.content)
}

// Close suggestions after asking
setActiveGroup(null)
}}
>
{memoizedSuggestion?.suggestions.map((suggestion, index) => (
<ToggleGroupItem
key={suggestion.content}
value={index.toString()}
tabIndex={0}
aria-label={`Select suggestion: ${suggestion.content}`}
asChild
>
<LemonButton
className="QuestionInput__QuestionSuggestion text-left"
role="button"
style={{ '--index': index } as React.CSSProperties}
size="small"
type="tertiary"
fullWidth
>
<span className="font-normal">{formatSuggestion(suggestion.content)}</span>
</LemonButton>
</ToggleGroupItem>
))}
</ToggleGroup>
</CSSTransition>
)
}
Loading