From 4abd345aba250788a66bb469e92a47f820b2e7f5 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Wed, 7 May 2025 19:21:05 +0200 Subject: [PATCH 01/24] feat: hardcoded suggestions --- .../src/scenes/max/QuestionSuggestions.tsx | 158 +++++++++--------- 1 file changed, 81 insertions(+), 77 deletions(-) diff --git a/frontend/src/scenes/max/QuestionSuggestions.tsx b/frontend/src/scenes/max/QuestionSuggestions.tsx index 77d1f661fec0..753877ad6784 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.tsx +++ b/frontend/src/scenes/max/QuestionSuggestions.tsx @@ -1,92 +1,96 @@ import { IconArrowUpRight, IconGear, IconShuffle } from '@posthog/icons' -import { LemonButton, LemonSkeleton } from '@posthog/lemon-ui' +import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { maxSettingsLogic } from 'scenes/settings/environment/maxSettingsLogic' +import { urls } from 'scenes/urls' import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' +import { productUrls } from '~/products' import { maxLogic } from './maxLogic' +const QUESTION_SUGGESTIONS = [ + { + label: 'Insights', + suggestions: [ + 'Create a funnel of the Pirate Metrics (AARRR)', + 'What are the most popular pages or screens?', + 'What is the distribution of paid vs organic traffic?', + 'What is the retention in the last two weeks?', + 'What are the top referring domains?', + 'Calculate a conversion rate for', + ], + }, + { + label: 'Docs', + suggestions: [ + 'How can I create a feature flag?', + 'Where do I watch session replays?', + 'Help me set up an experiment', + 'What is the autocapture?', + 'How can I capture an exception?', + ], + }, + { + label: 'Set up', + suggestions: [ + 'How can I set up session replay in', + 'How can I set up feature flags in', + 'How can I set up experiments in', + 'How can I set up data warehouse in', + 'How can I set up error tracking in', + 'How can I set up LLM Observability in', + 'How can I set up product analytics in', + ], + }, + { + label: 'SQL', + suggestions: ['Generate a SQL query to'], + url: urls.sqlEditor(), + }, + { + label: 'Session Replay', + suggestions: ['Find recordings'], + url: productUrls.replay(), + }, +] as const + export function QuestionSuggestions(): JSX.Element { - const { visibleSuggestions, allSuggestionsLoading, dataProcessingAccepted, tools } = useValues(maxLogic) - const { askMax, shuffleVisibleSuggestions } = useActions(maxLogic) - const { coreMemoryLoading, coreMemory } = useValues(maxSettingsLogic) + const { dataProcessingAccepted } = useValues(maxLogic) + const { shuffleVisibleSuggestions } = useActions(maxLogic) const { openSettingsPanel } = useActions(sidePanelSettingsLogic) - if (tools.length > 0) { - return <> - } - - if (!coreMemoryLoading && !coreMemory) { - return ( - askMax('Ready, steady, go!')} - disabledReason={!dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined} - center - > - Let's get started with me learning about your project! - - ) - } - return (
- { - coreMemoryLoading || allSuggestionsLoading ? ( - Array.from({ length: 3 }).map((_, index) => ( - - - - )) - ) : visibleSuggestions ? ( - <> - {visibleSuggestions.map((suggestion, index) => ( - askMax(suggestion)} - size="xsmall" - type="secondary" - sideIcon={} - center - className="shrink" - disabledReason={ - !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined - } - > - {suggestion} - - ))} -
- } - tooltip="Shuffle suggestions" - /> - - openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' }) - } - size="xsmall" - type="secondary" - icon={} - tooltip="Edit Max's memory" - /> -
- - ) : null /* Some error */ - } + {QUESTION_SUGGESTIONS.map((suggestion, index) => ( + askMax()} + size="xsmall" + type="secondary" + sideIcon={} + center + className="shrink" + disabledReason={!dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined} + > + {suggestion.label} + + ))} +
+ } + tooltip="Shuffle suggestions" + /> + openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' })} + size="xsmall" + type="secondary" + icon={} + tooltip="Edit Max's memory" + /> +
) } From 1a7b05dc730bd06dbf794965bf3a8d04d92a7ce3 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Fri, 9 May 2025 17:47:07 +0200 Subject: [PATCH 02/24] feat: animation --- .../src/scenes/max/QuestionSuggestions.scss | 18 +++ .../src/scenes/max/QuestionSuggestions.tsx | 133 ++++++++++++++---- .../scenes/max/questionSuggestionsLogic.ts | 35 +++++ 3 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 frontend/src/scenes/max/QuestionSuggestions.scss create mode 100644 frontend/src/scenes/max/questionSuggestionsLogic.ts diff --git a/frontend/src/scenes/max/QuestionSuggestions.scss b/frontend/src/scenes/max/QuestionSuggestions.scss new file mode 100644 index 000000000000..eb4c4a28668c --- /dev/null +++ b/frontend/src/scenes/max/QuestionSuggestions.scss @@ -0,0 +1,18 @@ +.QuestionSuggestion { + opacity: 1; + animation-name: QuestionSuggestion__slide-in-fade-down; + animation-duration: 0.3s; + animation-timing-function: ease-out; +} + +@keyframes QuestionSuggestion__slide-in-fade-down { + from { + opacity: 0; + transform: translateY(-10px); /* Adjust as needed */ + } + + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/frontend/src/scenes/max/QuestionSuggestions.tsx b/frontend/src/scenes/max/QuestionSuggestions.tsx index 753877ad6784..d7092d36d10f 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.tsx +++ b/frontend/src/scenes/max/QuestionSuggestions.tsx @@ -1,16 +1,29 @@ -import { IconArrowUpRight, IconGear, IconShuffle } from '@posthog/icons' +import './QuestionSuggestions.scss' + +import { IconBook, IconChevronLeft, IconGear, IconGraph, IconHogQL, IconPlug, IconRewindPlay } from '@posthog/icons' import { LemonButton } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' +import { useActions, useMountedLogic, useValues } from 'kea' import { urls } from 'scenes/urls' import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' import { productUrls } from '~/products' import { maxLogic } from './maxLogic' +import { questionSuggestionsLogic } from './questionSuggestionsLogic' + +// Define and export SuggestionGroup type +export interface SuggestionGroup { + readonly label: string + readonly icon: JSX.Element + readonly suggestions: readonly string[] + readonly url?: string +} -const QUESTION_SUGGESTIONS = [ +// Export the data and type it with SuggestionGroup +export const QUESTION_SUGGESTIONS_DATA: readonly SuggestionGroup[] = [ { label: 'Insights', + icon: , suggestions: [ 'Create a funnel of the Pirate Metrics (AARRR)', 'What are the most popular pages or screens?', @@ -22,6 +35,7 @@ const QUESTION_SUGGESTIONS = [ }, { label: 'Docs', + icon: , suggestions: [ 'How can I create a feature flag?', 'Where do I watch session replays?', @@ -32,6 +46,7 @@ const QUESTION_SUGGESTIONS = [ }, { label: 'Set up', + icon: , suggestions: [ 'How can I set up session replay in', 'How can I set up feature flags in', @@ -44,45 +59,107 @@ const QUESTION_SUGGESTIONS = [ }, { label: 'SQL', + icon: , suggestions: ['Generate a SQL query to'], url: urls.sqlEditor(), }, { label: 'Session Replay', + icon: , suggestions: ['Find recordings'], url: productUrls.replay(), }, -] as const +] as const // as const is good for type inference, but SuggestionGroup[] ensures structure export function QuestionSuggestions(): JSX.Element { const { dataProcessingAccepted } = useValues(maxLogic) - const { shuffleVisibleSuggestions } = useActions(maxLogic) + const { askMax } = useActions(maxLogic) // Changed submit to askMax const { openSettingsPanel } = useActions(sidePanelSettingsLogic) + const logic = useMountedLogic(questionSuggestionsLogic) + const { activeSuggestionGroup } = useValues(logic) + const { setActiveSuggestionGroupLabel } = useActions(logic) + + // Key for re-rendering suggestion list to help reset animations + const suggestionListKey = activeSuggestionGroup ? activeSuggestionGroup.label : 'categories' + return ( -
- {QUESTION_SUGGESTIONS.map((suggestion, index) => ( - askMax()} - size="xsmall" - type="secondary" - sideIcon={} - center - className="shrink" - disabledReason={!dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined} - > - {suggestion.label} - - ))} -
- } - tooltip="Shuffle suggestions" - /> +
+ {!activeSuggestionGroup ? ( + <> +

Ask Max about

+
+ {QUESTION_SUGGESTIONS_DATA.map((group) => ( + setActiveSuggestionGroupLabel(group.label)} + size="xsmall" + type="secondary" + icon={group.icon} + center + className="shrink" + disabledReason={ + !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + } + > + {group.label} + + ))} +
+ + ) : ( +
+
+ } + onClick={() => setActiveSuggestionGroupLabel(null)} + size="small" + type="tertiary" + > + Back + +

+ {activeSuggestionGroup.label} +

+
+ {' '} + {/* Replaced inline style with Tailwind class */} + {activeSuggestionGroup.url && ( + + Go to {activeSuggestionGroup.label} + + )} +
+
+ +
+ {activeSuggestionGroup.suggestions.map((suggestionText: string, index: number) => ( +
+ { + askMax(suggestionText) // Changed submit to askMax + setActiveSuggestionGroupLabel(null) // Close suggestions after asking + }} + size="small" + type="tertiary" + disabledReason={ + !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + } + title={suggestionText} + className="justify-start text-left truncate" + > + {suggestionText} + +
+ ))} +
+
+ )} +
openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' })} size="xsmall" diff --git a/frontend/src/scenes/max/questionSuggestionsLogic.ts b/frontend/src/scenes/max/questionSuggestionsLogic.ts new file mode 100644 index 000000000000..d5aa659b83b1 --- /dev/null +++ b/frontend/src/scenes/max/questionSuggestionsLogic.ts @@ -0,0 +1,35 @@ +import { kea } from 'kea' + +// We will import QUESTION_SUGGESTIONS_DATA and SuggestionGroup from QuestionSuggestions.tsx +import { QUESTION_SUGGESTIONS_DATA, type SuggestionGroup } from './QuestionSuggestions' +import type { questionSuggestionsLogicType } from './questionSuggestionsLogicType' + +export const questionSuggestionsLogic = kea({ + path: ['scenes', 'max', 'questionSuggestionsLogic'], + actions: { + setActiveSuggestionGroupLabel: (label: string | null) => ({ label }), + }, + reducers: { + activeSuggestionGroupLabel: [ + null as string | null, + { + setActiveSuggestionGroupLabel: (_: any, { label }: { label: string | null }): string | null => label, + }, + ], + }, + selectors: { + suggestionGroups: [ + () => [], // Kea selector typing hint + (): readonly SuggestionGroup[] => QUESTION_SUGGESTIONS_DATA, + ], + activeSuggestionGroup: [ + (s) => [s.activeSuggestionGroupLabel, s.suggestionGroups], + (label: string | null, groups: readonly SuggestionGroup[]): SuggestionGroup | undefined => { + if (!label) { + return undefined + } + return groups.find((sg: SuggestionGroup) => sg.label === label) + }, + ], + }, +}) From 22358c61afc39a7efdec53ec9d5acbe8edc20df7 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Wed, 21 May 2025 20:57:53 +0200 Subject: [PATCH 03/24] feat: render elements & focus of elements --- frontend/src/scenes/max/QuestionInput.tsx | 5 +- .../src/scenes/max/QuestionSuggestions.tsx | 343 ++++++++++++------ frontend/src/scenes/max/maxLogic.tsx | 6 + 3 files changed, 248 insertions(+), 106 deletions(-) diff --git a/frontend/src/scenes/max/QuestionInput.tsx b/frontend/src/scenes/max/QuestionInput.tsx index 418341e4a370..208ddd1dd4f8 100644 --- a/frontend/src/scenes/max/QuestionInput.tsx +++ b/frontend/src/scenes/max/QuestionInput.tsx @@ -16,7 +16,7 @@ interface QuestionInputProps { export function QuestionInput({ isFloating }: QuestionInputProps): JSX.Element { const { tools } = useValues(maxGlobalLogic) - const { question, threadLoading, inputDisabled, submissionDisabledReason } = useValues(maxLogic) + const { question, threadLoading, inputDisabled, submissionDisabledReason, focusCounter } = useValues(maxLogic) const { askMax, setQuestion, stopGeneration } = useActions(maxLogic) const textAreaRef = useRef(null) @@ -29,11 +29,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]) return (
, + suggestions: [ + { + label: 'Generate an SQL query to', + content: 'Generate an SQL query to ', + }, + ], + url: urls.sqlEditor(), + tooltip: 'Max can generate SQL queries using your PostHog data and the data warehouse.', + }, + { + label: 'Product Analytics', icon: , suggestions: [ - 'Create a funnel of the Pirate Metrics (AARRR)', - 'What are the most popular pages or screens?', - 'What is the distribution of paid vs organic traffic?', - 'What is the retention in the last two weeks?', - 'What are the top referring domains?', - 'Calculate a conversion rate for', + { + label: 'Create a funnel of the Pirate Metrics (AARRR)', + content: 'Create a funnel of the Pirate Metrics (AARRR)', + }, + { + label: 'What are the most popular pages or screens?', + content: 'What are the most popular pages or screens?', + }, + { + label: 'What is the retention in the last two weeks?', + content: 'What is the retention in the last two weeks?', + }, + { + label: 'What are the top referring domains?', + content: 'What are the top referring domains?', + }, + { + label: 'Calculate a conversion rate for ', + content: 'Calculate a conversion rate for ', + }, ], + tooltip: 'Max can generate insights from natural language and tweak existing ones.', }, { label: 'Docs', icon: , suggestions: [ - 'How can I create a feature flag?', - 'Where do I watch session replays?', - 'Help me set up an experiment', - 'What is the autocapture?', - 'How can I capture an exception?', + { + label: 'How can I create a feature flag?', + }, + { + label: 'Where do I watch session replays?', + }, + { + label: 'Help me set up an experiment', + }, + { + label: 'What is the autocapture?', + }, + { + label: 'How can I capture an exception?', + }, ], + tooltip: 'Max can help you find the right documentation pieces.', }, { label: 'Set up', icon: , suggestions: [ - 'How can I set up session replay in', - 'How can I set up feature flags in', - 'How can I set up experiments in', - 'How can I set up data warehouse in', - 'How can I set up error tracking in', - 'How can I set up LLM Observability in', - 'How can I set up product analytics in', + { + label: 'How can I set up session replay in ', + content: 'How can I set up session replay in ', + }, + { + label: 'How can I set up feature flags in...', + content: 'How can I set up feature flags in ', + }, + { + label: 'How can I set up experiments in...', + content: 'How can I set up experiments in ', + }, + { + label: 'How can I set up data warehouse in...', + content: 'How can I set up data warehouse in ', + }, + { + label: 'How can I set up error tracking in...', + content: 'How can I set up error tracking in ', + }, + { + label: 'How can I set up LLM Observability in...', + content: 'How can I set up LLM Observability in', + }, + { + label: 'How can I set up product analytics in...', + content: 'How can I set up product analytics in', + }, ], - }, - { - label: 'SQL', - icon: , - suggestions: ['Generate a SQL query to'], - url: urls.sqlEditor(), + tooltip: 'Max can help you set up PostHog SDKs in your stack.', }, { label: 'Session Replay', icon: , - suggestions: ['Find recordings'], + suggestions: [ + { + label: 'Find recordings ', + content: 'Find recordings ', + }, + ], url: productUrls.replay(), + tooltip: 'Max can find session recordings for you.', }, ] as const // as const is good for type inference, but SuggestionGroup[] ensures structure export function QuestionSuggestions(): JSX.Element { + const suggestionListRef = useRef(null) + const { dataProcessingAccepted } = useValues(maxLogic) - const { askMax } = useActions(maxLogic) // Changed submit to askMax + const { askMax, setQuestion, focusInput } = useActions(maxLogic) // Changed submit to askMax const { openSettingsPanel } = useActions(sidePanelSettingsLogic) const logic = useMountedLogic(questionSuggestionsLogic) @@ -83,91 +155,156 @@ export function QuestionSuggestions(): JSX.Element { // Key for re-rendering suggestion list to help reset animations const suggestionListKey = activeSuggestionGroup ? activeSuggestionGroup.label : 'categories' + useClickOutside([suggestionListRef], () => { + if (activeSuggestionGroup) { + setActiveSuggestionGroupLabel(null) + } + }) + return (
- {!activeSuggestionGroup ? ( - <> -

Ask Max about

-
- {QUESTION_SUGGESTIONS_DATA.map((group) => ( +

Ask Max about

+
+ {!activeSuggestionGroup ? ( + <> +
    + {QUESTION_SUGGESTIONS_DATA.map((group) => ( +
  • + { + // If it's a product-based skill, open the URL first + if ( + group.url && + !router.values.currentLocation.pathname.includes(group.url) + ) { + router.actions.push(group.url) + } + + // If there's only one suggestion, we can just ask Max directly + if (group.suggestions.length <= 1) { + if (group.suggestions[0].content) { + // Content requires to write something to continue + setQuestion(group.suggestions[0].content) + focusInput() + } else { + // Otherwise, just launch the generation + askMax(group.suggestions[0].label) + } + } else { + setActiveSuggestionGroupLabel(group.label) + } + }} + size="xsmall" + type="secondary" + icon={group.icon} + center + disabledReason={ + !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + } + tooltip={group.tooltip} + > + {group.label} + +
  • + ))} +
  • + + openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' }) + } + size="xsmall" + type="secondary" + icon={} + tooltip="Edit Max's memory" + /> +
  • +
+ + ) : ( +
+
setActiveSuggestionGroupLabel(group.label)} - size="xsmall" - type="secondary" - icon={group.icon} - center - className="shrink" - disabledReason={ - !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined - } + icon={} + onClick={() => setActiveSuggestionGroupLabel(null)} + size="small" + type="tertiary" > - {group.label} + Back - ))} -
- - ) : ( -
-
- } - onClick={() => setActiveSuggestionGroupLabel(null)} - size="small" - type="tertiary" - > - Back - -

- {activeSuggestionGroup.label} -

-
- {' '} - {/* Replaced inline style with Tailwind class */} - {activeSuggestionGroup.url && ( - - Go to {activeSuggestionGroup.label} - - )} +

+ {activeSuggestionGroup.label} +

+
+ {' '} + {/* Replaced inline style with Tailwind class */} + {activeSuggestionGroup.url && ( + + Go to {activeSuggestionGroup.label} + + )} +
-
-
- {activeSuggestionGroup.suggestions.map((suggestionText: string, index: number) => ( -
- { - askMax(suggestionText) // Changed submit to askMax - setActiveSuggestionGroupLabel(null) // Close suggestions after asking - }} - size="small" - type="tertiary" - disabledReason={ - !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined - } - title={suggestionText} - className="justify-start text-left truncate" +
+ {activeSuggestionGroup.suggestions.map((suggestion, index) => ( +
- {suggestionText} - -
- ))} + { + if (suggestion.content) { + // Content requires to write something to continue + setQuestion(suggestion.content) + focusInput() + } else { + // Otherwise, just launch the generation + askMax(suggestion.label) + } + + // Close suggestions after asking + setActiveSuggestionGroupLabel(null) + }} + size="small" + type="tertiary" + disabledReason={ + !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + } + title={suggestion.label} + className="justify-start text-left truncate" + > + {suggestion.label} + +
+ ))} +
-
- )} -
- openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' })} - size="xsmall" - type="secondary" - icon={} - tooltip="Edit Max's memory" - /> + )}
) } + +function useClickOutside( + elementRefs: MutableRefObject[], + callback: (event: MouseEvent) => void +): void { + const callbackRef = useRef(callback) + callbackRef.current = callback + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + const target = event.target + if (!(target instanceof Node) || elementRefs.every((elementRef) => !elementRef.current?.contains(target))) { + callbackRef.current?.(event) + } + } + document.addEventListener('click', handleClickOutside, true) + return () => document.removeEventListener('click', handleClickOutside, true) + }, [elementRefs]) +} diff --git a/frontend/src/scenes/max/maxLogic.tsx b/frontend/src/scenes/max/maxLogic.tsx index 422820eed0dc..7b408703e5e8 100644 --- a/frontend/src/scenes/max/maxLogic.tsx +++ b/frontend/src/scenes/max/maxLogic.tsx @@ -97,6 +97,7 @@ export const maxLogic = kea([ pollConversation: (currentRecursionDepth: number = 0) => ({ currentRecursionDepth }), goBack: true, setBackScreen: (screen: 'history') => ({ screen }), + focusInput: true, }), reducers({ @@ -193,6 +194,11 @@ export const maxLogic = kea([ startNewConversation: () => null, }, ], + + /** + * When the focus counter updates, the input component will rerender and refocus the input. + */ + focusCounter: [0, { focusInput: (state) => state + 1 }], }), loaders({ From 479c86596668a4e30f1c8e6d846274addbea8e79 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Thu, 22 May 2025 17:06:18 +0200 Subject: [PATCH 04/24] feat: animations --- frontend/src/scenes/max/HistoryPreview.tsx | 2 +- frontend/src/scenes/max/Max.tsx | 54 +++-- .../src/scenes/max/QuestionSuggestions.scss | 9 +- .../src/scenes/max/QuestionSuggestions.tsx | 224 ++++++++---------- 4 files changed, 128 insertions(+), 161 deletions(-) diff --git a/frontend/src/scenes/max/HistoryPreview.tsx b/frontend/src/scenes/max/HistoryPreview.tsx index f3345eac2d3f..d341e7e2046e 100644 --- a/frontend/src/scenes/max/HistoryPreview.tsx +++ b/frontend/src/scenes/max/HistoryPreview.tsx @@ -19,7 +19,7 @@ export function HistoryPreview({ sidePanel = false }: HistoryPreviewProps): JSX. } return ( -
+

Recent chats

- {wasUserAutoEnrolled && ( - setWasUserAutoEnrolled(false)} - > - PostHog AI feature preview{' '} - +
+ {wasUserAutoEnrolled && ( + setWasUserAutoEnrolled(false)} > - activated - - ! - - )} -
- - + PostHog AI feature preview{' '} + + activated + + ! + + )} +
+ + +
+
+
+
-
) : ( <> diff --git a/frontend/src/scenes/max/QuestionSuggestions.scss b/frontend/src/scenes/max/QuestionSuggestions.scss index eb4c4a28668c..81d97bc3dd8e 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.scss +++ b/frontend/src/scenes/max/QuestionSuggestions.scss @@ -1,14 +1,13 @@ .QuestionSuggestion { - opacity: 1; - animation-name: QuestionSuggestion__slide-in-fade-down; - animation-duration: 0.3s; - animation-timing-function: ease-out; + opacity: 0; + animation: QuestionSuggestion__slide-in-fade-down 75ms ease-out forwards; + animation-delay: calc(var(--index) * 50ms); } @keyframes QuestionSuggestion__slide-in-fade-down { from { opacity: 0; - transform: translateY(-10px); /* Adjust as needed */ + transform: translateY(-1.5rem); } to { diff --git a/frontend/src/scenes/max/QuestionSuggestions.tsx b/frontend/src/scenes/max/QuestionSuggestions.tsx index 0853f314145d..1b340b81aa63 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.tsx +++ b/frontend/src/scenes/max/QuestionSuggestions.tsx @@ -1,6 +1,6 @@ import './QuestionSuggestions.scss' -import { IconBook, IconChevronLeft, IconGear, IconGraph, IconHogQL, IconPlug, IconRewindPlay } from '@posthog/icons' +import { IconBook, IconGear, IconGraph, IconHogQL, IconPlug, IconRewindPlay } from '@posthog/icons' import { LemonButton } from '@posthog/lemon-ui' import { useActions, useMountedLogic, useValues } from 'kea' import { router } from 'kea-router' @@ -22,15 +22,15 @@ export interface SuggestionItem { content?: string } export interface SuggestionGroup { - readonly label: string - readonly icon: JSX.Element - readonly suggestions: readonly SuggestionItem[] - readonly url?: string - readonly tooltip?: string + label: string + icon: JSX.Element + suggestions: SuggestionItem[] + url?: string + tooltip?: string } // Export the data and type it with SuggestionGroup -export const QUESTION_SUGGESTIONS_DATA: readonly SuggestionGroup[] = [ +export const QUESTION_SUGGESTIONS_DATA: SuggestionGroup[] = [ { label: 'SQL', icon: , @@ -64,7 +64,7 @@ export const QUESTION_SUGGESTIONS_DATA: readonly SuggestionGroup[] = [ content: 'What are the top referring domains?', }, { - label: 'Calculate a conversion rate for
', + label: 'Calculate a conversion rate for ', content: 'Calculate a conversion rate for ', }, ], @@ -139,22 +139,19 @@ export const QUESTION_SUGGESTIONS_DATA: readonly SuggestionGroup[] = [ url: productUrls.replay(), tooltip: 'Max can find session recordings for you.', }, -] as const // as const is good for type inference, but SuggestionGroup[] ensures structure +] as const export function QuestionSuggestions(): JSX.Element { - const suggestionListRef = useRef(null) + const suggestionListRef = useRef(null) const { dataProcessingAccepted } = useValues(maxLogic) - const { askMax, setQuestion, focusInput } = useActions(maxLogic) // Changed submit to askMax + const { askMax, setQuestion, focusInput } = useActions(maxLogic) const { openSettingsPanel } = useActions(sidePanelSettingsLogic) const logic = useMountedLogic(questionSuggestionsLogic) const { activeSuggestionGroup } = useValues(logic) const { setActiveSuggestionGroupLabel } = useActions(logic) - // Key for re-rendering suggestion list to help reset animations - const suggestionListKey = activeSuggestionGroup ? activeSuggestionGroup.label : 'categories' - useClickOutside([suggestionListRef], () => { if (activeSuggestionGroup) { setActiveSuggestionGroupLabel(null) @@ -163,130 +160,97 @@ export function QuestionSuggestions(): JSX.Element { return (
-

Ask Max about

-
- {!activeSuggestionGroup ? ( - <> -
    - {QUESTION_SUGGESTIONS_DATA.map((group) => ( -
  • - { - // If it's a product-based skill, open the URL first - if ( - group.url && - !router.values.currentLocation.pathname.includes(group.url) - ) { - router.actions.push(group.url) - } +

    + {activeSuggestionGroup ? activeSuggestionGroup.label : 'Ask Max about'} +

    + {!activeSuggestionGroup ? ( +
      + {QUESTION_SUGGESTIONS_DATA.map((group) => ( +
    • + { + // If it's a product-based skill, open the URL first + if (group.url && !router.values.currentLocation.pathname.includes(group.url)) { + router.actions.push(group.url) + } - // If there's only one suggestion, we can just ask Max directly - if (group.suggestions.length <= 1) { - if (group.suggestions[0].content) { - // Content requires to write something to continue - setQuestion(group.suggestions[0].content) - focusInput() - } else { - // Otherwise, just launch the generation - askMax(group.suggestions[0].label) - } - } else { - setActiveSuggestionGroupLabel(group.label) - } - }} - size="xsmall" - type="secondary" - icon={group.icon} - center - disabledReason={ - !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + // If there's only one suggestion, we can just ask Max directly + if (group.suggestions.length <= 1) { + if (group.suggestions[0].content) { + // Content requires to write something to continue + setQuestion(group.suggestions[0].content) + focusInput() + } else { + // Otherwise, just launch the generation + askMax(group.suggestions[0].label) } - tooltip={group.tooltip} - > - {group.label} - -
    • - ))} -
    • - - openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' }) + } else { + setActiveSuggestionGroupLabel(group.label) } - size="xsmall" - type="secondary" - icon={} - tooltip="Edit Max's memory" - /> -
    • -
    - - ) : ( -
    -
    + }} + size="xsmall" + type="secondary" + icon={group.icon} + center + disabledReason={ + !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + } + tooltip={group.tooltip} + > + {group.label} + +
  • + ))} +
  • + + openSettingsPanel({ sectionId: 'environment-max', settingId: 'core-memory' }) + } + size="xsmall" + type="secondary" + icon={} + tooltip="Edit Max's memory" + /> +
  • +
+ ) : ( +
    + {activeSuggestionGroup.suggestions.map((suggestion, index) => ( +
  • } - onClick={() => setActiveSuggestionGroupLabel(null)} + fullWidth + onClick={() => { + if (suggestion.content) { + // Content requires to write something to continue + setQuestion(suggestion.content) + focusInput() + } else { + // Otherwise, just launch the generation + askMax(suggestion.label) + } + + // Close suggestions after asking + setActiveSuggestionGroupLabel(null) + }} size="small" type="tertiary" + disabledReason={ + !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined + } + title={suggestion.label} + className="text-left [&_span]:line-clamp-1" > - Back + {suggestion.label} -

    - {activeSuggestionGroup.label} -

    -
    - {' '} - {/* Replaced inline style with Tailwind class */} - {activeSuggestionGroup.url && ( - - Go to {activeSuggestionGroup.label} - - )} -
    -
- -
- {activeSuggestionGroup.suggestions.map((suggestion, index) => ( -
- { - if (suggestion.content) { - // Content requires to write something to continue - setQuestion(suggestion.content) - focusInput() - } else { - // Otherwise, just launch the generation - askMax(suggestion.label) - } - - // Close suggestions after asking - setActiveSuggestionGroupLabel(null) - }} - size="small" - type="tertiary" - disabledReason={ - !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined - } - title={suggestion.label} - className="justify-start text-left truncate" - > - {suggestion.label} - -
- ))} -
-
- )} -
+ + ))} + + )}
) } From f1e27f71a486344a8c49c12c9b440f426e323faf Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Thu, 22 May 2025 19:38:32 +0200 Subject: [PATCH 05/24] feat: better copies --- .../src/scenes/max/QuestionSuggestions.scss | 2 +- .../src/scenes/max/QuestionSuggestions.tsx | 38 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/frontend/src/scenes/max/QuestionSuggestions.scss b/frontend/src/scenes/max/QuestionSuggestions.scss index 81d97bc3dd8e..5858bc097fee 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.scss +++ b/frontend/src/scenes/max/QuestionSuggestions.scss @@ -1,6 +1,6 @@ .QuestionSuggestion { opacity: 0; - animation: QuestionSuggestion__slide-in-fade-down 75ms ease-out forwards; + animation: QuestionSuggestion__slide-in-fade-down 50ms ease-out forwards; animation-delay: calc(var(--index) * 50ms); } diff --git a/frontend/src/scenes/max/QuestionSuggestions.tsx b/frontend/src/scenes/max/QuestionSuggestions.tsx index 1b340b81aa63..948b6eab074b 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.tsx +++ b/frontend/src/scenes/max/QuestionSuggestions.tsx @@ -49,19 +49,15 @@ export const QUESTION_SUGGESTIONS_DATA: SuggestionGroup[] = [ suggestions: [ { label: 'Create a funnel of the Pirate Metrics (AARRR)', - content: 'Create a funnel of the Pirate Metrics (AARRR)', }, { label: 'What are the most popular pages or screens?', - content: 'What are the most popular pages or screens?', }, { label: 'What is the retention in the last two weeks?', - content: 'What is the retention in the last two weeks?', }, { label: 'What are the top referring domains?', - content: 'What are the top referring domains?', }, { label: 'Calculate a conversion rate for ', @@ -84,7 +80,7 @@ export const QUESTION_SUGGESTIONS_DATA: SuggestionGroup[] = [ label: 'Help me set up an experiment', }, { - label: 'What is the autocapture?', + label: 'Explain autocapture', }, { label: 'How can I capture an exception?', @@ -97,32 +93,32 @@ export const QUESTION_SUGGESTIONS_DATA: SuggestionGroup[] = [ icon: , suggestions: [ { - label: 'How can I set up session replay in ', - content: 'How can I set up session replay in ', + label: 'How can I set up the session replay in ', + content: 'How can I set up the session replay in ', }, { - label: 'How can I set up feature flags in...', - content: 'How can I set up feature flags in ', + label: 'How can I set up the feature flags in...', + content: 'How can I set up the feature flags in ', }, { - label: 'How can I set up experiments in...', - content: 'How can I set up experiments in ', + label: 'How can I set up the experiments in...', + content: 'How can I set up the experiments in ', }, { - label: 'How can I set up data warehouse in...', - content: 'How can I set up data warehouse in ', + label: 'How can I set up the data warehouse in...', + content: 'How can I set up the data warehouse in ', }, { - label: 'How can I set up error tracking in...', - content: 'How can I set up error tracking in ', + label: 'How can I set up the error tracking in...', + content: 'How can I set up the error tracking in ', }, { - label: 'How can I set up LLM Observability in...', - content: 'How can I set up LLM Observability in', + label: 'How can I set up the LLM Observability in...', + content: 'How can I set up the LLM Observability in', }, { - label: 'How can I set up product analytics in...', - content: 'How can I set up product analytics in', + label: 'How can I set up the product analytics in...', + content: 'How can I set up the product analytics in', }, ], tooltip: 'Max can help you set up PostHog SDKs in your stack.', @@ -132,8 +128,8 @@ export const QUESTION_SUGGESTIONS_DATA: SuggestionGroup[] = [ icon: , suggestions: [ { - label: 'Find recordings ', - content: 'Find recordings ', + label: 'Find recordings for ', + content: 'Find recordings for ', }, ], url: productUrls.replay(), From 12589f2d9549e89ae104307a11156b2de882d23f Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Thu, 22 May 2025 20:21:31 +0200 Subject: [PATCH 06/24] feat: prevent layout shift --- frontend/src/scenes/max/Max.tsx | 8 ++++---- frontend/src/scenes/max/QuestionSuggestions.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/scenes/max/Max.tsx b/frontend/src/scenes/max/Max.tsx index 9ac48afbd5bc..a72e805ba4c2 100644 --- a/frontend/src/scenes/max/Max.tsx +++ b/frontend/src/scenes/max/Max.tsx @@ -174,8 +174,8 @@ export function MaxInstance({ sidePanel }: MaxInstanceProps): JSX.Element { // pb-7 below is intentionally specific - it's chosen so that the bottom-most chat's title // is at the same viewport height as the QuestionInput text that appear after going into a thread. // This makes the transition from one view into another just that bit smoother visually. -
-
+
+
{wasUserAutoEnrolled && ( )} -
+
-
+
diff --git a/frontend/src/scenes/max/QuestionSuggestions.tsx b/frontend/src/scenes/max/QuestionSuggestions.tsx index 948b6eab074b..59d05b174e86 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.tsx +++ b/frontend/src/scenes/max/QuestionSuggestions.tsx @@ -239,7 +239,7 @@ export function QuestionSuggestions(): JSX.Element { !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined } title={suggestion.label} - className="text-left [&_span]:line-clamp-1" + className="[&_span]:line-clamp-1 [&>span]:flex" > {suggestion.label} From c95b2c84ff8590c2191882cef64f8f3475abf774 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Thu, 22 May 2025 20:33:57 +0200 Subject: [PATCH 07/24] fix: break all --- frontend/src/scenes/max/QuestionSuggestions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/scenes/max/QuestionSuggestions.tsx b/frontend/src/scenes/max/QuestionSuggestions.tsx index 59d05b174e86..b95e8c6a7c12 100644 --- a/frontend/src/scenes/max/QuestionSuggestions.tsx +++ b/frontend/src/scenes/max/QuestionSuggestions.tsx @@ -239,7 +239,7 @@ export function QuestionSuggestions(): JSX.Element { !dataProcessingAccepted ? 'Please accept OpenAI processing data' : undefined } title={suggestion.label} - className="[&_span]:line-clamp-1 [&>span]:flex" + className="[&_span]:line-clamp-1 [&_span]:break-all [&>span]:flex" > {suggestion.label} From 2c29befa7dc1e346b698a7b2f70693cceabb8d1f Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Thu, 22 May 2025 21:02:29 +0200 Subject: [PATCH 08/24] fix: logic --- frontend/src/scenes/max/Max.stories.tsx | 17 ++ .../src/scenes/max/QuestionSuggestions.tsx | 142 +-------------- .../max/maxQuestionSuggestionsLogic.tsx | 164 ++++++++++++++++++ .../scenes/max/questionSuggestionsLogic.ts | 35 ---- 4 files changed, 190 insertions(+), 168 deletions(-) create mode 100644 frontend/src/scenes/max/maxQuestionSuggestionsLogic.tsx delete mode 100644 frontend/src/scenes/max/questionSuggestionsLogic.ts diff --git a/frontend/src/scenes/max/Max.stories.tsx b/frontend/src/scenes/max/Max.stories.tsx index 636ccfdc5e42..b427180e8b6d 100644 --- a/frontend/src/scenes/max/Max.stories.tsx +++ b/frontend/src/scenes/max/Max.stories.tsx @@ -17,6 +17,7 @@ import { import conversationList from './__mocks__/conversationList.json' import { MaxInstance, MaxInstanceProps } from './Max' import { maxLogic } from './maxLogic' +import { maxQuestionSuggestionsLogic } from './maxQuestionSuggestionsLogic' const meta: Meta = { title: 'Scenes-App/Max AI', @@ -358,3 +359,19 @@ ChatHistoryLoading.parameters = { waitForLoadersToDisappear: false, }, } + +export const ThreadWithOpenedSuggestions: StoryFn = () => { + const { setActiveGroup } = useActions(maxQuestionSuggestionsLogic) + + useEffect(() => { + // The largest group is the set up group + setActiveGroup(3) + }, []) + + return