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