Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions static/app/views/seerExplorer/askUserQuestionBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function AskUserQuestionBlock({
checked={isOtherSelected}
onChange={() => handleOptionClick(optionsCount)}
name={`question-${questionIndex}`}
size="sm"
/>
<CustomInputWrapper>
<CustomInput
Expand Down
4 changes: 3 additions & 1 deletion static/app/views/seerExplorer/blockComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@ export default BlockComponent;

const Block = styled('div')<{isFocused?: boolean; isLast?: boolean}>`
width: 100%;
border-bottom: ${p => (p.isLast ? 'none' : `1px solid ${p.theme.border}`)};
border-top: 1px solid transparent;
border-bottom: ${p =>
p.isLast ? '1px solid transparent' : `1px solid ${p.theme.border}`};
position: relative;
flex-shrink: 0; /* Prevent blocks from shrinking */
cursor: pointer;
Expand Down
85 changes: 66 additions & 19 deletions static/app/views/seerExplorer/explorerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@ import styled from '@emotion/styled';
import moment from 'moment-timezone';

import TimeSince from 'sentry/components/timeSince';
import {space} from 'sentry/styles/space';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import {useExplorerSessions} from 'sentry/views/seerExplorer/hooks/useExplorerSessions';

type MenuMode =
| 'slash-commands-keyboard'
| 'slash-commands-manual'
| 'session-history'
| 'hidden';
type MenuMode = 'slash-commands-keyboard' | 'session-history' | 'hidden';

interface ExplorerMenuProps {
clearInput: () => void;
Expand All @@ -26,6 +21,8 @@ interface ExplorerMenuProps {
onNew: () => void;
};
textAreaRef: React.RefObject<HTMLTextAreaElement | null>;
inputAnchorRef?: React.RefObject<HTMLElement | null>;
menuAnchorRef?: React.RefObject<HTMLElement | null>;
}

interface MenuItemProps {
Expand All @@ -44,8 +41,16 @@ export function useExplorerMenu({
panelVisible,
slashCommandHandlers,
onChangeSession,
menuAnchorRef,
inputAnchorRef,
}: ExplorerMenuProps) {
const [menuMode, setMenuMode] = useState<MenuMode>('hidden');
const [menuPosition, setMenuPosition] = useState<{
bottom?: string | number;
left?: string | number;
right?: string | number;
top?: string | number;
}>({});

const allSlashCommands = useSlashCommands(slashCommandHandlers);

Expand All @@ -67,14 +72,12 @@ export function useExplorerMenu({
switch (menuMode) {
case 'slash-commands-keyboard':
return filteredSlashCommands;
case 'slash-commands-manual':
return allSlashCommands;
case 'session-history':
return sessionItems;
default:
return [];
}
}, [menuMode, allSlashCommands, filteredSlashCommands, sessionItems]);
}, [menuMode, filteredSlashCommands, sessionItems]);

const close = useCallback(() => {
setMenuMode('hidden');
Expand Down Expand Up @@ -199,9 +202,54 @@ export function useExplorerMenu({
return undefined;
}, [handleKeyDown, isVisible]);

// Calculate menu position based on anchor element
useEffect(() => {
if (!isVisible) {
setMenuPosition({});
return;
}

const anchorRef =
menuMode === 'slash-commands-keyboard' ? inputAnchorRef : menuAnchorRef;
const isSlashCommand = menuMode === 'slash-commands-keyboard';

if (!anchorRef?.current) {
setMenuPosition({
bottom: '100%',
left: '16px',
});
return;
}

const rect = anchorRef.current.getBoundingClientRect();
const panelRect = anchorRef.current
.closest('[data-seer-explorer-root]')
?.getBoundingClientRect();

if (!panelRect) {
return;
}

const spacing = 8;
const relativeTop = rect.top - panelRect.top;
const relativeLeft = rect.left - panelRect.left;

setMenuPosition(
isSlashCommand
? {
bottom: `${panelRect.height - relativeTop + spacing}px`,
left: `${relativeLeft}px`,
}
: {
top: `${relativeTop + rect.height + spacing}px`,
left: `${relativeLeft}px`,
}
);
}, [isVisible, menuMode, menuAnchorRef, inputAnchorRef]);

const menu = (
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<MenuPanel panelSize={panelSize}>
<MenuPanel panelSize={panelSize} style={menuPosition} data-seer-menu-panel="">
{menuItems.map((item, index) => (
<MenuItem
key={item.key}
Expand Down Expand Up @@ -230,21 +278,22 @@ export function useExplorerMenu({
</Activity>
);

// Handler for the button entrypoint.
const onMenuButtonClick = useCallback(() => {
if (menuMode === 'hidden') {
setMenuMode('slash-commands-manual');
} else {
// Handler for opening session history from button
const openSessionHistory = useCallback(() => {
if (menuMode === 'session-history') {
close();
} else {
setMenuMode('session-history');
refetchSessions();
}
}, [menuMode, setMenuMode, close]);
}, [menuMode, close, refetchSessions]);

return {
menu,
menuMode,
isMenuOpen: menuMode !== 'hidden',
closeMenu: close,
onMenuButtonClick,
openSessionHistory,
};
}

Expand Down Expand Up @@ -350,8 +399,6 @@ const MenuPanel = styled('div')<{
panelSize: 'max' | 'med';
}>`
position: absolute;
bottom: 100%;
left: ${space(2)};
width: 300px;
background: ${p => p.theme.background};
border: 1px solid ${p => p.theme.border};
Expand Down
75 changes: 71 additions & 4 deletions static/app/views/seerExplorer/explorerPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {createPortal} from 'react-dom';

import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import useOrganization from 'sentry/utils/useOrganization';
import AskUserQuestionBlock from 'sentry/views/seerExplorer/askUserQuestionBlock';
import BlockComponent from 'sentry/views/seerExplorer/blockComponents';
Expand All @@ -15,6 +16,7 @@ import InputSection from 'sentry/views/seerExplorer/inputSection';
import PanelContainers, {
BlocksContainer,
} from 'sentry/views/seerExplorer/panelContainers';
import TopBar from 'sentry/views/seerExplorer/topBar';
import type {Block, ExplorerPanelProps} from 'sentry/views/seerExplorer/types';

function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
Expand All @@ -33,6 +35,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
const hoveredBlockIndex = useRef<number>(-1);
const userScrolledUpRef = useRef<boolean>(false);
const allowHoverFocusChange = useRef<boolean>(true);
const sessionHistoryButtonRef = useRef<HTMLButtonElement>(null);

// Custom hooks
const {panelSize, handleMaxSize, handleMedSize} = usePanelSizing();
Expand Down Expand Up @@ -204,7 +207,9 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
setIsMinimized(false);
}, [setFocusedBlockIndex, textareaRef, setIsMinimized]);

const {menu, isMenuOpen, closeMenu, onMenuButtonClick} = useExplorerMenu({
const openFeedbackForm = useFeedbackForm();

const {menu, isMenuOpen, menuMode, closeMenu, openSessionHistory} = useExplorerMenu({
clearInput: () => setInputValue(''),
inputValue,
focusInput,
Expand All @@ -217,13 +222,44 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
onNew: startNewSession,
},
onChangeSession: setRunId,
menuAnchorRef: sessionHistoryButtonRef,
inputAnchorRef: textareaRef,
});

const handlePanelBackgroundClick = useCallback(() => {
setIsMinimized(false);
closeMenu();
}, [closeMenu]);

// Close menu when clicking outside of it
useEffect(() => {
if (!isMenuOpen) {
return undefined;
}

const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const menuElement = document.querySelector('[data-seer-menu-panel]');

// Don't close if clicking on the menu itself or the button
if (
menuElement?.contains(target) ||
sessionHistoryButtonRef.current?.contains(target)
) {
return;
}

// Close menu when clicking anywhere else
closeMenu();
};

// Use capture phase to catch clicks before they bubble
document.addEventListener('mousedown', handleClickOutside, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
};
}, [isMenuOpen, closeMenu]);

const handleInputClick = useCallback(() => {
// Click handler for the input textarea.
focusInput();
Expand All @@ -250,6 +286,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {

const isAwaitingUserInput = sessionData?.status === 'awaiting_user_input';
const pendingInput = sessionData?.pending_user_input;
const isEmptyState = blocks.length === 0 && !(isAwaitingUserInput && pendingInput);

const {
isFileApprovalPending,
Expand Down Expand Up @@ -360,6 +397,26 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
},
});

const handleFeedbackClick = useCallback(() => {
if (openFeedbackForm) {
openFeedbackForm({
formTitle: 'Seer Explorer Feedback',
messagePlaceholder: 'How can we make Seer Explorer better for you?',
tags: {
['feedback.source']: 'seer_explorer',
},
});
}
}, [openFeedbackForm]);

const handleSizeToggle = useCallback(() => {
if (panelSize === 'max') {
handleMedSize();
} else {
handleMaxSize();
}
}, [panelSize, handleMaxSize, handleMedSize]);

const panelContent = (
<PanelContainers
ref={panelRef}
Expand All @@ -368,8 +425,20 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
panelSize={panelSize}
onUnminimize={handleUnminimize}
>
<TopBar
isEmptyState={isEmptyState}
isPolling={isPolling}
isSessionHistoryOpen={isMenuOpen && menuMode === 'session-history'}
onFeedbackClick={handleFeedbackClick}
onNewChatClick={startNewSession}
onSessionHistoryClick={openSessionHistory}
onSizeToggleClick={handleSizeToggle}
panelSize={panelSize}
sessionHistoryButtonRef={sessionHistoryButtonRef}
/>
{menu}
<BlocksContainer ref={scrollContainerRef} onClick={handlePanelBackgroundClick}>
{blocks.length === 0 && !(isAwaitingUserInput && pendingInput) ? (
{isEmptyState ? (
<EmptyState />
) : (
<Fragment>
Expand Down Expand Up @@ -439,8 +508,6 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
)}
</BlocksContainer>
<InputSection
menu={menu}
onMenuButtonClick={onMenuButtonClick}
focusedBlockIndex={focusedBlockIndex}
inputValue={inputValue}
interruptRequested={interruptRequested}
Expand Down
28 changes: 1 addition & 27 deletions static/app/views/seerExplorer/inputSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {Container, Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import {TextArea} from '@sentry/scraps/textarea/textarea';

import {IconMenu} from 'sentry/icons';
import {t} from 'sentry/locale';

interface FileApprovalActions {
Expand All @@ -33,12 +32,10 @@ interface InputSectionProps {
inputValue: string;
interruptRequested: boolean;
isPolling: boolean;
menu: React.ReactElement;
onClear: () => void;
onInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onInputClick: () => void;
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onMenuButtonClick: () => void;
textAreaRef: React.RefObject<HTMLTextAreaElement | null>;
fileApprovalActions?: FileApprovalActions;
isMinimized?: boolean;
Expand All @@ -47,8 +44,6 @@ interface InputSectionProps {
}

function InputSection({
menu,
onMenuButtonClick,
inputValue,
focusedBlockIndex,
isMinimized = false,
Expand Down Expand Up @@ -224,16 +219,7 @@ function InputSection({

return (
<InputBlock>
{menu}
<InputRow>
<ButtonContainer>
<Button
priority="default"
aria-label="Toggle Menu"
onClick={onMenuButtonClick}
icon={<IconMenu size="md" />}
/>
</ButtonContainer>
<InputTextarea
ref={textAreaRef}
value={inputValue}
Expand Down Expand Up @@ -266,21 +252,9 @@ const InputRow = styled('div')`
padding: 0;
`;

const ButtonContainer = styled('div')`
display: flex;
align-items: center;
padding: ${p => p.theme.space.sm};
padding-top: ${p => p.theme.space.md};

button {
width: auto;
padding: ${p => p.theme.space.md};
}
`;

const InputTextarea = styled(TextArea)`
width: 100%;
margin: ${p => p.theme.space.sm} ${p => p.theme.space.sm} ${p => p.theme.space.sm} 0;
margin: ${p => p.theme.space.sm};
color: ${p => p.theme.textColor};
resize: none;
overflow-y: auto;
Expand Down
Loading
Loading