1
- import { useRef } from 'react' ;
1
+ import { useEffect , useRef , useState } from 'react' ;
2
2
import styled from '@emotion/styled' ;
3
3
import { useOption } from '@react-aria/listbox' ;
4
4
import type { ComboBoxState } from '@react-stately/combobox' ;
5
5
6
- import { FeatureBadge } from 'sentry/components/core/badge/featureBadge' ;
6
+ import { promptsUpdate } from 'sentry/actionCreators/prompts' ;
7
+ import { FeatureBadge } from 'sentry/components/core/badge/featureBadge' ;
7
8
import InteractionStateLayer from 'sentry/components/core/interactionStateLayer' ;
9
+ import { useOrganizationSeerSetup } from 'sentry/components/events/autofix/useOrganizationSeerSetup' ;
10
+ import ExternalLink from 'sentry/components/links/externalLink' ;
11
+ import LoadingIndicator from 'sentry/components/loadingIndicator' ;
8
12
import { useSearchQueryBuilder } from 'sentry/components/searchQueryBuilder/context' ;
9
13
import { IconSeer } from 'sentry/icons' ;
10
- import { t } from 'sentry/locale' ;
14
+ import { t , tct } from 'sentry/locale' ;
11
15
import { space } from 'sentry/styles/space' ;
12
16
import { trackAnalytics } from 'sentry/utils/analytics' ;
17
+ import { useIsMutating , useMutation , useQueryClient } from 'sentry/utils/queryClient' ;
18
+ import useApi from 'sentry/utils/useApi' ;
13
19
import useOrganization from 'sentry/utils/useOrganization' ;
14
20
15
21
export const ASK_SEER_ITEM_KEY = 'ask_seer' ;
16
22
export const ASK_SEER_CONSENT_ITEM_KEY = 'ask_seer_consent' ;
17
23
24
+ const setupCheckQueryKey = ( orgSlug : string ) =>
25
+ `/organizations/${ orgSlug } /seer/setup-check/` ;
26
+
27
+ function AskSeerConsentOption < T > ( { state} : { state : ComboBoxState < T > } ) {
28
+ const api = useApi ( ) ;
29
+ const queryClient = useQueryClient ( ) ;
30
+ const organization = useOrganization ( ) ;
31
+ const itemRef = useRef < HTMLDivElement > ( null ) ;
32
+ const linkRef = useRef < HTMLAnchorElement > ( null ) ;
33
+ const [ optionDisableOverride , setOptionDisableOverride ] = useState ( false ) ;
34
+
35
+ useEffect ( ( ) => {
36
+ const link = linkRef . current ;
37
+ if ( ! link ) return undefined ;
38
+
39
+ const disableOption = ( ) => setOptionDisableOverride ( true ) ;
40
+ const enableOption = ( ) => setOptionDisableOverride ( false ) ;
41
+
42
+ link . addEventListener ( 'mouseover' , disableOption ) ;
43
+ link . addEventListener ( 'mouseout' , enableOption ) ;
44
+
45
+ return ( ) => {
46
+ link . removeEventListener ( 'mouseover' , disableOption ) ;
47
+ link . removeEventListener ( 'mouseout' , enableOption ) ;
48
+ } ;
49
+ } , [ ] ) ;
50
+
51
+ const seerAcknowledgeMutation = useMutation ( {
52
+ mutationKey : [ setupCheckQueryKey ( organization . slug ) ] ,
53
+ mutationFn : ( ) => {
54
+ return promptsUpdate ( api , {
55
+ organization,
56
+ feature : 'seer_autofix_setup_acknowledged' ,
57
+ status : 'dismissed' ,
58
+ } ) ;
59
+ } ,
60
+ onSuccess : ( ) => {
61
+ queryClient . invalidateQueries ( {
62
+ queryKey : [ setupCheckQueryKey ( organization . slug ) ] ,
63
+ } ) ;
64
+ } ,
65
+ } ) ;
66
+
67
+ const { optionProps, labelProps, isFocused, isPressed} = useOption (
68
+ {
69
+ key : ASK_SEER_CONSENT_ITEM_KEY ,
70
+ 'aria-label' : 'Enable Gen AI' ,
71
+ shouldFocusOnHover : true ,
72
+ shouldSelectOnPressUp : true ,
73
+ isDisabled : optionDisableOverride ,
74
+ } ,
75
+ state ,
76
+ itemRef
77
+ ) ;
78
+
79
+ const handleClick = ( ) => {
80
+ trackAnalytics ( 'trace.explorer.ai_query_interface' , {
81
+ organization,
82
+ action : 'consent_accepted' ,
83
+ } ) ;
84
+ seerAcknowledgeMutation . mutate ( ) ;
85
+ } ;
86
+
87
+ return (
88
+ < AskSeerListItem
89
+ ref = { itemRef }
90
+ onClick = { handleClick }
91
+ { ...optionProps }
92
+ justifyContent = "space-between"
93
+ >
94
+ < InteractionStateLayer isHovered = { isFocused } isPressed = { isPressed } />
95
+ < div style = { { display : 'flex' , alignItems : 'center' , gap : space ( 1 ) } } >
96
+ < IconSeer />
97
+ < AskSeerLabel { ...labelProps } >
98
+ { t ( 'Enable Gen AI' ) } < FeatureBadge type = "beta" />
99
+ </ AskSeerLabel >
100
+ </ div >
101
+ < SeerConsentText >
102
+ { tct (
103
+ 'Query assistant requires Generative AI which is subject to our [dataProcessingPolicy:data processing policy].' ,
104
+ {
105
+ dataProcessingPolicy : (
106
+ < TooltipSubExternalLink
107
+ ref = { linkRef }
108
+ href = "https://docs.sentry.io/product/security/ai-ml-policy/#use-of-identifying-data-for-generative-ai-features"
109
+ />
110
+ ) ,
111
+ }
112
+ ) }
113
+ </ SeerConsentText >
114
+ </ AskSeerListItem >
115
+ ) ;
116
+ }
117
+
18
118
function AskSeerOption < T > ( { state} : { state : ComboBoxState < T > } ) {
19
119
const ref = useRef < HTMLDivElement > ( null ) ;
20
120
const { setDisplaySeerResults} = useSearchQueryBuilder ( ) ;
@@ -26,6 +126,7 @@ function AskSeerOption<T>({state}: {state: ComboBoxState<T>}) {
26
126
'aria-label' : 'Ask Seer' ,
27
127
shouldFocusOnHover : true ,
28
128
shouldSelectOnPressUp : true ,
129
+ isDisabled : false ,
29
130
} ,
30
131
state ,
31
132
ref
@@ -51,13 +152,63 @@ function AskSeerOption<T>({state}: {state: ComboBoxState<T>}) {
51
152
}
52
153
53
154
export function AskSeer < T > ( { state} : { state : ComboBoxState < T > } ) {
155
+ const organization = useOrganization ( ) ;
156
+ const { gaveSeerConsentRef} = useSearchQueryBuilder ( ) ;
157
+ const isMutating = useIsMutating ( {
158
+ mutationKey : [ setupCheckQueryKey ( organization . slug ) ] ,
159
+ } ) ;
160
+
161
+ const { setupAcknowledgement, isPending : isPendingSetupCheck } =
162
+ useOrganizationSeerSetup ( ) ;
163
+ const orgHasAcknowledged = setupAcknowledgement . orgHasAcknowledged ;
164
+
165
+ if ( ! gaveSeerConsentRef . current && orgHasAcknowledged && ! isPendingSetupCheck ) {
166
+ gaveSeerConsentRef . current = true ;
167
+ }
168
+
169
+ if ( isPendingSetupCheck || isMutating ) {
170
+ return (
171
+ < AskSeerPane >
172
+ < AskSeerListItem >
173
+ < AskSeerLabel width = "auto" > { t ( 'Loading Seer' ) } </ AskSeerLabel >
174
+ < LoadingIndicator size = { 16 } style = { { margin : 0 } } />
175
+ </ AskSeerListItem >
176
+ </ AskSeerPane >
177
+ ) ;
178
+ }
179
+
180
+ if ( orgHasAcknowledged ) {
181
+ return (
182
+ < AskSeerPane >
183
+ < AskSeerOption state = { state } />
184
+ </ AskSeerPane >
185
+ ) ;
186
+ }
187
+
54
188
return (
55
189
< AskSeerPane >
56
- < AskSeerOption state = { state } />
190
+ < AskSeerConsentOption state = { state } />
57
191
</ AskSeerPane >
58
192
) ;
59
193
}
60
194
195
+ const TooltipSubExternalLink = styled ( ExternalLink ) `
196
+ color: ${ p => p . theme . purple400 } ;
197
+
198
+ :hover {
199
+ color: ${ p => p . theme . purple400 } ;
200
+ text-decoration: underline;
201
+ }
202
+ ` ;
203
+
204
+ const SeerConsentText = styled ( 'p' ) `
205
+ color: ${ p => p . theme . subText } ;
206
+ font-size: ${ p => p . theme . fontSize . xs } ;
207
+ font-weight: ${ p => p . theme . fontWeight . normal } ;
208
+ margin: 0;
209
+ background-color: none;
210
+ ` ;
211
+
61
212
const AskSeerPane = styled ( 'div' ) `
62
213
grid-area: seer;
63
214
display: flex;
@@ -69,7 +220,7 @@ const AskSeerPane = styled('div')`
69
220
width: 100%;
70
221
` ;
71
222
72
- const AskSeerListItem = styled ( 'div' ) `
223
+ const AskSeerListItem = styled ( 'div' ) < { justifyContent ?: 'flex-start' | 'space-between' } > `
73
224
position: relative;
74
225
display: flex;
75
226
align-items: center;
@@ -83,7 +234,7 @@ const AskSeerListItem = styled('div')`
83
234
font-size: ${ p => p . theme . fontSize . md } ;
84
235
font-weight: ${ p => p . theme . fontWeight . bold } ;
85
236
text-align: left;
86
- justify-content: flex-start;
237
+ justify-content: ${ p => p . justifyContent ?? ' flex-start' } ;
87
238
gap: ${ space ( 1 ) } ;
88
239
list-style: none;
89
240
margin: 0;
@@ -99,12 +250,13 @@ const AskSeerListItem = styled('div')`
99
250
}
100
251
` ;
101
252
102
- const AskSeerLabel = styled ( 'span' ) `
253
+ const AskSeerLabel = styled ( 'span' ) < { width ?: 'auto' } > `
103
254
${ p => p . theme . overflowEllipsis } ;
104
255
color: ${ p => p . theme . purple400 } ;
105
256
font-size: ${ p => p . theme . fontSize . md } ;
106
257
font-weight: ${ p => p . theme . fontWeight . bold } ;
107
258
display: flex;
108
259
align-items: center;
109
260
gap: ${ space ( 1 ) } ;
261
+ width: ${ p => p . width } ;
110
262
` ;
0 commit comments