Skip to content

Commit 6d2e7ea

Browse files
authored
chore(trace ai queries): Seer button keyboard support (#93305)
- Support selecting the "Ask Seer" button in the dropdown with the arrow keys - Button only appears if Generative AI features are turned on in settings https://github.com/user-attachments/assets/8456529f-30d9-4c04-ae81-80e8a380648a
1 parent 3de5fa7 commit 6d2e7ea

File tree

5 files changed

+147
-44
lines changed

5 files changed

+147
-44
lines changed

static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import useOrganization from 'sentry/utils/useOrganization';
3434
import usePrevious from 'sentry/utils/usePrevious';
3535
import {useTraceExploreAiQueryContext} from 'sentry/views/explore/contexts/traceExploreAiQueryContext';
3636

37+
const ASK_SEER_ITEM_KEY = 'ask_seer';
38+
3739
interface FilterKeyListBoxProps<T> extends CustomComboboxMenuProps<T> {
3840
recentFilters: Array<TokenResult<Token.FILTER>>;
3941
sections: Section[];
@@ -138,6 +140,39 @@ function RecentSearchFilterOption<T>({
138140
);
139141
}
140142

143+
function AskSeerOption<T>({state}: {state: ComboBoxState<T>}) {
144+
const ref = useRef<HTMLLIElement>(null);
145+
const {setDisplaySeerResults} = useSearchQueryBuilder();
146+
const organization = useOrganization();
147+
148+
const {optionProps, labelProps, isFocused, isPressed} = useOption(
149+
{
150+
key: ASK_SEER_ITEM_KEY,
151+
'aria-label': 'Ask Seer',
152+
shouldFocusOnHover: true,
153+
shouldSelectOnPressUp: true,
154+
},
155+
state,
156+
ref
157+
);
158+
159+
const handleClick = () => {
160+
trackAnalytics('trace.explorer.ai_query_interface', {
161+
organization,
162+
action: 'opened',
163+
});
164+
setDisplaySeerResults(true);
165+
};
166+
167+
return (
168+
<AskSeerListItem ref={ref} onClick={handleClick} {...optionProps}>
169+
<InteractionStateLayer isHovered={isFocused} isPressed={isPressed} />
170+
<IconSeer />
171+
<AskSeerLabel {...labelProps}>{t('Ask Seer')}</AskSeerLabel>
172+
</AskSeerListItem>
173+
);
174+
}
175+
141176
function useHighlightFirstOptionOnSectionChange({
142177
state,
143178
selectedSection,
@@ -215,9 +250,12 @@ function FilterKeyMenuContent<T extends SelectOptionOrSectionWithKey<string>>({
215250
fullWidth,
216251
sections,
217252
}: FilterKeyMenuContentProps<T>) {
218-
const {filterKeys, setDisplaySeerResults} = useSearchQueryBuilder();
219-
const focusedItem = state.collection.getItem(state.selectionManager.focusedKey ?? '')
220-
?.props?.value as string | undefined;
253+
const {filterKeys} = useSearchQueryBuilder();
254+
const focusedItem = state.selectionManager.focusedKey
255+
? (state.collection.getItem(state.selectionManager.focusedKey)?.props?.value as
256+
| string
257+
| undefined)
258+
: undefined;
221259
const focusedKey = focusedItem ? filterKeys[focusedItem] : null;
222260
const showRecentFilters = recentFilters.length > 0;
223261
const showDetailsPane = fullWidth && selectedSection !== RECENT_SEARCH_CATEGORY_VALUE;
@@ -228,28 +266,17 @@ function FilterKeyMenuContent<T extends SelectOptionOrSectionWithKey<string>>({
228266
const areAiFeaturesAllowed =
229267
!organization?.hideAiFeatures && organization.features.includes('gen-ai-features');
230268

269+
const showAskSeerOption = traceExploreAiQueryContext && areAiFeaturesAllowed;
270+
231271
return (
232272
<Fragment>
233-
<Feature features="organizations:gen-ai-explore-traces">
234-
{traceExploreAiQueryContext && areAiFeaturesAllowed ? (
235-
<SeerButtonWrapper>
236-
<SeerFullWidthButton
237-
size="md"
238-
icon={<IconSeer />}
239-
onClick={() => {
240-
trackAnalytics('trace.explorer.ai_query_interface', {
241-
organization,
242-
action: 'opened',
243-
});
244-
setDisplaySeerResults(true);
245-
}}
246-
borderless
247-
>
248-
{t('Ask Seer')}
249-
</SeerFullWidthButton>
250-
</SeerButtonWrapper>
251-
) : null}
252-
</Feature>
273+
{showAskSeerOption ? (
274+
<Feature features="organizations:gen-ai-explore-traces">
275+
<AskSeerPane>
276+
<AskSeerOption state={state} />
277+
</AskSeerPane>
278+
</Feature>
279+
) : null}
253280
{showRecentFilters ? (
254281
<RecentFiltersPane>
255282
{recentFilters.map(filter => (
@@ -332,19 +359,23 @@ export function FilterKeyListBox<T extends SelectOptionOrSectionWithKey<string>>
332359
const areAiFeaturesAllowed =
333360
!organization?.hideAiFeatures && organization.features.includes('gen-ai-features');
334361

335-
// Add recent filters to hiddenOptions so they don't show up the ListBox component.
336-
// We render recent filters manually in the RecentFiltersPane component.
337-
const hiddenOptionsWithRecentsAdded = useMemo<Set<SelectKey>>(() => {
338-
return new Set([
362+
const hiddenOptionsWithRecentsAndAskSeerAdded = useMemo<Set<SelectKey>>(() => {
363+
const baseHidden = [
339364
...hiddenOptions,
340365
...recentFilters.map(filter => createRecentFilterOptionKey(getKeyName(filter.key))),
341-
]);
342-
}, [hiddenOptions, recentFilters]);
366+
];
367+
368+
if (traceExploreAiQueryContext && areAiFeaturesAllowed) {
369+
baseHidden.push(ASK_SEER_ITEM_KEY);
370+
}
371+
372+
return new Set(baseHidden);
373+
}, [hiddenOptions, recentFilters, traceExploreAiQueryContext, areAiFeaturesAllowed]);
343374

344375
useHighlightFirstOptionOnSectionChange({
345376
state,
346377
selectedSection,
347-
hiddenOptions: hiddenOptionsWithRecentsAdded,
378+
hiddenOptions: hiddenOptionsWithRecentsAndAskSeerAdded,
348379
sections,
349380
isOpen,
350381
});
@@ -389,7 +420,7 @@ export function FilterKeyListBox<T extends SelectOptionOrSectionWithKey<string>>
389420
{isOpen ? (
390421
<FilterKeyMenuContent
391422
fullWidth={fullWidth}
392-
hiddenOptions={hiddenOptionsWithRecentsAdded}
423+
hiddenOptions={hiddenOptionsWithRecentsAndAskSeerAdded}
393424
listBoxProps={listBoxProps}
394425
listBoxRef={listBoxRef}
395426
recentFilters={recentFilters}
@@ -415,7 +446,7 @@ export function FilterKeyListBox<T extends SelectOptionOrSectionWithKey<string>>
415446
{isOpen ? (
416447
<FilterKeyMenuContent
417448
fullWidth={fullWidth}
418-
hiddenOptions={hiddenOptionsWithRecentsAdded}
449+
hiddenOptions={hiddenOptionsWithRecentsAndAskSeerAdded}
419450
listBoxProps={listBoxProps}
420451
listBoxRef={listBoxRef}
421452
recentFilters={recentFilters}
@@ -602,7 +633,7 @@ const EmptyState = styled('div')`
602633
}
603634
`;
604635

605-
const SeerButtonWrapper = styled('div')`
636+
const AskSeerPane = styled('div')`
606637
grid-area: seer;
607638
display: flex;
608639
align-items: center;
@@ -613,8 +644,13 @@ const SeerButtonWrapper = styled('div')`
613644
width: 100%;
614645
`;
615646

616-
const SeerFullWidthButton = styled(Button)`
647+
const AskSeerListItem = styled('li')`
648+
position: relative;
649+
display: flex;
650+
align-items: center;
617651
width: 100%;
652+
padding: ${space(1)} ${space(1.5)};
653+
background: transparent;
618654
border-radius: 0;
619655
background-color: none;
620656
box-shadow: none;
@@ -623,14 +659,24 @@ const SeerFullWidthButton = styled(Button)`
623659
font-weight: ${p => p.theme.fontWeight.bold};
624660
text-align: left;
625661
justify-content: flex-start;
626-
padding: ${space(1)} ${space(2)};
627-
display: flex;
628-
align-items: center;
629662
gap: ${space(1)};
663+
list-style: none;
664+
margin: 0;
665+
630666
&:hover,
631667
&:focus {
632668
background-color: ${p => p.theme.purple100};
633669
color: ${p => p.theme.purple400};
634-
box-shadow: none;
635670
}
671+
672+
&[aria-selected='true'] {
673+
background: ${p => p.theme.purple100};
674+
color: ${p => p.theme.purple400};
675+
}
676+
`;
677+
const AskSeerLabel = styled('span')`
678+
${p => p.theme.overflowEllipsis};
679+
color: ${p => p.theme.purple400};
680+
font-size: ${p => p.theme.fontSize.md};
681+
font-weight: ${p => p.theme.fontWeight.bold};
636682
`;

static/app/components/searchQueryBuilder/tokens/filterKeyListBox/types.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export interface RecentQueryItem extends SelectOptionWithKey<string> {
4747
value: string;
4848
}
4949

50+
export interface AskSeerItem extends SelectOptionWithKey<string> {
51+
hideCheck: boolean;
52+
type: 'ask-seer';
53+
value: string;
54+
}
55+
5056
export type SearchKeyItem =
5157
| KeySectionItem
5258
| KeyItem
@@ -61,7 +67,8 @@ export type FilterKeyItem =
6167
| RecentQueryItem
6268
| RawSearchItem
6369
| FilterValueItem
64-
| RawSearchFilterValueItem;
70+
| RawSearchFilterValueItem
71+
| AskSeerItem;
6572

6673
export type Section = {
6774
label: ReactNode;

static/app/components/searchQueryBuilder/tokens/filterKeyListBox/useFilterKeyListBox.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {useRecentSearchFilters} from 'sentry/components/searchQueryBuilder/token
1616
import {
1717
ALL_CATEGORY,
1818
ALL_CATEGORY_VALUE,
19+
createAskSeerItem,
1920
createRecentFilterItem,
2021
createRecentFilterOptionKey,
2122
createRecentQueryItem,
@@ -28,8 +29,11 @@ import type {FieldDefinitionGetter} from 'sentry/components/searchQueryBuilder/t
2829
import type {Token, TokenResult} from 'sentry/components/searchSyntax/parser';
2930
import {getKeyName} from 'sentry/components/searchSyntax/utils';
3031
import type {RecentSearch, TagCollection} from 'sentry/types/group';
32+
import {trackAnalytics} from 'sentry/utils/analytics';
3133
import clamp from 'sentry/utils/number/clamp';
34+
import useOrganization from 'sentry/utils/useOrganization';
3235
import usePrevious from 'sentry/utils/usePrevious';
36+
import {useTraceExploreAiQueryContext} from 'sentry/views/explore/contexts/traceExploreAiQueryContext';
3337

3438
const MAX_OPTIONS_WITHOUT_SEARCH = 100;
3539
const MAX_OPTIONS_WITH_SEARCH = 8;
@@ -159,19 +163,28 @@ function useFilterKeySections({
159163
return {sections, selectedSection, setSelectedSection};
160164
}
161165
export function useFilterKeyListBox({filterValue}: {filterValue: string}) {
162-
const {filterKeys, getFieldDefinition} = useSearchQueryBuilder();
166+
const {filterKeys, getFieldDefinition, setDisplaySeerResults} = useSearchQueryBuilder();
163167
const {sectionedItems} = useFilterKeyItems();
164168
const recentFilters = useRecentSearchFilters();
165169
const {data: recentSearches} = useRecentSearches();
166170
const {sections, selectedSection, setSelectedSection} = useFilterKeySections({
167171
recentSearches,
168172
});
169173

174+
const organization = useOrganization();
175+
const traceExploreAiQueryContext = useTraceExploreAiQueryContext();
176+
const areAiFeaturesAllowed =
177+
!organization?.hideAiFeatures && organization.features.includes('gen-ai-features');
178+
170179
const filterKeyMenuItems = useMemo(() => {
171180
const recentFilterItems = makeRecentFilterItems({recentFilters});
172181

182+
const askSeerItem =
183+
traceExploreAiQueryContext && areAiFeaturesAllowed ? [createAskSeerItem()] : [];
184+
173185
if (selectedSection === RECENT_SEARCH_CATEGORY_VALUE) {
174186
return [
187+
...askSeerItem,
175188
...recentFilterItems,
176189
...makeRecentSearchQueryItems({
177190
recentSearches,
@@ -192,14 +205,16 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) {
192205
return true;
193206
});
194207

195-
return [...recentFilterItems, ...filteredByCategory];
208+
return [...askSeerItem, ...recentFilterItems, ...filteredByCategory];
196209
}, [
197210
filterKeys,
198211
getFieldDefinition,
199212
recentFilters,
200213
recentSearches,
201214
sectionedItems,
202215
selectedSection,
216+
traceExploreAiQueryContext,
217+
areAiFeaturesAllowed,
203218
]);
204219

205220
const customMenu: CustomComboboxMenu<FilterKeyItem> = props => {
@@ -358,11 +373,26 @@ export function useFilterKeyListBox({filterValue}: {filterValue: string}) {
358373
[handleArrowUpDown, handleCycleRecentFilterKeys, handleCycleSections]
359374
);
360375

376+
const handleOptionSelected = useCallback(
377+
(option: FilterKeyItem) => {
378+
if (option.type === 'ask-seer') {
379+
trackAnalytics('trace.explorer.ai_query_interface', {
380+
organization,
381+
action: 'opened',
382+
});
383+
setDisplaySeerResults(true);
384+
return;
385+
}
386+
},
387+
[organization, setDisplaySeerResults]
388+
);
389+
361390
return {
362391
sectionItems: filterKeyMenuItems,
363392
customMenu: shouldShowExplorationMenu ? customMenu : undefined,
364393
maxOptions:
365394
filterValue.length === 0 ? MAX_OPTIONS_WITHOUT_SEARCH : MAX_OPTIONS_WITH_SEARCH,
366395
onKeyDownCapture: shouldShowExplorationMenu ? onKeyDownCapture : undefined,
396+
handleOptionSelected,
367397
};
368398
}

static/app/components/searchQueryBuilder/tokens/filterKeyListBox/utils.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {getEscapedKey} from 'sentry/components/core/compactSelect/utils';
44
import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery';
55
import {KeyDescription} from 'sentry/components/searchQueryBuilder/tokens/filterKeyListBox/keyDescription';
66
import type {
7+
AskSeerItem,
78
FilterValueItem,
89
KeyItem,
910
KeySectionItem,
@@ -198,6 +199,17 @@ export function createRecentQueryItem({
198199
};
199200
}
200201

202+
export function createAskSeerItem(): AskSeerItem {
203+
return {
204+
key: getEscapedKey('ask_seer'),
205+
value: 'ask_seer',
206+
textValue: 'Ask Seer',
207+
type: 'ask-seer' as const,
208+
label: t('Ask Seer'),
209+
hideCheck: true,
210+
};
211+
}
212+
201213
const SearchItemLabel = styled('div')`
202214
color: ${p => p.theme.subText};
203215
white-space: nowrap;

static/app/components/searchQueryBuilder/tokens/freeText.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,10 @@ function SearchQueryBuilderInputInternal({
273273
recentSearches,
274274
} = useSearchQueryBuilder();
275275

276-
const {customMenu, sectionItems, maxOptions, onKeyDownCapture} = useFilterKeyListBox({
277-
filterValue,
278-
});
276+
const {customMenu, sectionItems, maxOptions, onKeyDownCapture, handleOptionSelected} =
277+
useFilterKeyListBox({
278+
filterValue,
279+
});
279280
const sortedFilteredItems = useSortedFilterKeyItems({
280281
filterValue,
281282
inputValue,
@@ -392,6 +393,13 @@ function SearchQueryBuilderInputInternal({
392393
items={items}
393394
placeholder={query === '' ? placeholder : undefined}
394395
onOptionSelected={option => {
396+
if (handleOptionSelected) {
397+
handleOptionSelected(option);
398+
if (option.type === 'ask-seer') {
399+
return;
400+
}
401+
}
402+
395403
if (option.type === 'recent-query') {
396404
dispatch({
397405
type: 'UPDATE_QUERY',

0 commit comments

Comments
 (0)