Skip to content

Commit 1fa6e7a

Browse files
authored
feat(explorer): add ui for questions tool (#104410)
Adds an interface for the agent to ask users multiple choice questions <img width="843" height="448" alt="Screenshot 2025-12-04 at 11 46 36 AM" src="https://github.com/user-attachments/assets/b49ccc2a-706f-427d-8537-1c6793470556" /> Part of AIML-1694
1 parent 2233b18 commit 1fa6e7a

File tree

11 files changed

+831
-69
lines changed

11 files changed

+831
-69
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import {useEffect, useRef} from 'react';
2+
import styled from '@emotion/styled';
3+
import {AnimatePresence, motion} from 'framer-motion';
4+
5+
import {Flex} from '@sentry/scraps/layout';
6+
import {Text} from '@sentry/scraps/text';
7+
8+
import {Input} from 'sentry/components/core/input';
9+
import {Radio} from 'sentry/components/core/radio';
10+
import type {Question} from 'sentry/views/seerExplorer/hooks/usePendingUserInput';
11+
12+
interface AskUserQuestionBlockProps {
13+
currentQuestion: Question;
14+
customText: string;
15+
isOtherSelected: boolean;
16+
onCustomTextChange: (text: string) => void;
17+
onSelectOption: (index: number) => void;
18+
questionIndex: number;
19+
selectedOption: number;
20+
isFocused?: boolean;
21+
isLast?: boolean;
22+
onClick?: () => void;
23+
onMouseEnter?: () => void;
24+
onMouseLeave?: () => void;
25+
}
26+
27+
function AskUserQuestionBlock({
28+
currentQuestion,
29+
customText,
30+
isFocused,
31+
isLast,
32+
isOtherSelected,
33+
onClick,
34+
onCustomTextChange,
35+
onMouseEnter,
36+
onMouseLeave,
37+
onSelectOption,
38+
questionIndex,
39+
selectedOption,
40+
}: AskUserQuestionBlockProps) {
41+
const customInputRef = useRef<HTMLInputElement>(null);
42+
43+
const optionsCount = currentQuestion.options.length;
44+
45+
// Auto-focus the custom input when "Other" is selected, blur when not
46+
useEffect(() => {
47+
if (isOtherSelected) {
48+
customInputRef.current?.focus();
49+
} else {
50+
customInputRef.current?.blur();
51+
}
52+
}, [isOtherSelected]);
53+
54+
const handleOptionClick = (index: number) => {
55+
onSelectOption(index);
56+
};
57+
58+
return (
59+
<Block
60+
isFocused={isFocused}
61+
isLast={isLast}
62+
onClick={onClick}
63+
onMouseEnter={onMouseEnter}
64+
onMouseLeave={onMouseLeave}
65+
>
66+
<AnimatePresence>
67+
<motion.div
68+
initial={{opacity: 0, y: 10}}
69+
animate={{opacity: 1, y: 0}}
70+
exit={{opacity: 0, y: 10}}
71+
>
72+
<Flex align="start" width="100%">
73+
<BlockContentWrapper>
74+
<AnimatePresence mode="wait">
75+
<motion.div
76+
key={questionIndex}
77+
initial={{opacity: 0, x: 20}}
78+
animate={{opacity: 1, x: 0}}
79+
exit={{opacity: 0, x: -20}}
80+
transition={{duration: 0.12, ease: 'easeOut'}}
81+
>
82+
<QuestionContainer>
83+
<Text>{currentQuestion.question}</Text>
84+
<OptionsContainer>
85+
{currentQuestion.options.map((option, index) => (
86+
<OptionRow
87+
key={index}
88+
onClick={() => handleOptionClick(index)}
89+
isSelected={selectedOption === index}
90+
>
91+
<Radio
92+
checked={selectedOption === index}
93+
onChange={() => handleOptionClick(index)}
94+
name={`question-${questionIndex}`}
95+
size="sm"
96+
/>
97+
<OptionContent>
98+
<Text size="sm">{option.label}</Text>
99+
<Text variant="muted" size="sm">
100+
{option.description}
101+
</Text>
102+
</OptionContent>
103+
</OptionRow>
104+
))}
105+
{/* Custom text input option (always visible) */}
106+
<OptionRow
107+
onClick={() => handleOptionClick(optionsCount)}
108+
isSelected={isOtherSelected}
109+
>
110+
<Radio
111+
checked={isOtherSelected}
112+
onChange={() => handleOptionClick(optionsCount)}
113+
name={`question-${questionIndex}`}
114+
size="sm"
115+
/>
116+
<CustomInputWrapper>
117+
<CustomInput
118+
ref={customInputRef}
119+
value={customText}
120+
onChange={e => onCustomTextChange(e.target.value)}
121+
onClick={e => {
122+
e.stopPropagation();
123+
handleOptionClick(optionsCount);
124+
}}
125+
placeholder="Type your own answer..."
126+
size="sm"
127+
/>
128+
</CustomInputWrapper>
129+
</OptionRow>
130+
</OptionsContainer>
131+
</QuestionContainer>
132+
</motion.div>
133+
</AnimatePresence>
134+
</BlockContentWrapper>
135+
</Flex>
136+
</motion.div>
137+
</AnimatePresence>
138+
</Block>
139+
);
140+
}
141+
142+
export default AskUserQuestionBlock;
143+
144+
const Block = styled('div')<{isFocused?: boolean; isLast?: boolean}>`
145+
width: 100%;
146+
border-bottom: ${p => (p.isLast ? 'none' : `1px solid ${p.theme.border}`)};
147+
position: relative;
148+
flex-shrink: 0;
149+
cursor: pointer;
150+
background: ${p => (p.isFocused ? p.theme.hover : 'transparent')};
151+
`;
152+
153+
const BlockContentWrapper = styled('div')`
154+
flex: 1;
155+
min-width: 0;
156+
overflow: hidden;
157+
padding: ${p => p.theme.space.xl};
158+
`;
159+
160+
const QuestionContainer = styled('div')`
161+
display: flex;
162+
flex-direction: column;
163+
gap: ${p => p.theme.space.lg};
164+
`;
165+
166+
const OptionsContainer = styled('div')`
167+
display: flex;
168+
flex-direction: column;
169+
gap: ${p => p.theme.space.sm};
170+
`;
171+
172+
const OptionRow = styled('div')<{isSelected: boolean}>`
173+
display: flex;
174+
align-items: center;
175+
gap: ${p => p.theme.space.md};
176+
padding: ${p => p.theme.space.sm} ${p => p.theme.space.sm};
177+
border-radius: ${p => p.theme.borderRadius};
178+
cursor: pointer;
179+
transition: background-color 0.15s ease;
180+
181+
&:hover {
182+
background: ${p => p.theme.backgroundSecondary};
183+
}
184+
`;
185+
186+
const OptionContent = styled('div')`
187+
display: flex;
188+
flex-direction: column;
189+
gap: ${p => p.theme.space['2xs']};
190+
flex: 1;
191+
padding-top: 2px;
192+
`;
193+
194+
const CustomInputWrapper = styled('div')`
195+
flex: 1;
196+
`;
197+
198+
const CustomInput = styled(Input)`
199+
width: 100%;
200+
`;

static/app/views/seerExplorer/blockComponents.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface BlockProps {
2020
block: Block;
2121
blockIndex: number;
2222
isAwaitingFileApproval?: boolean;
23+
isAwaitingQuestion?: boolean;
2324
isFocused?: boolean;
2425
isLast?: boolean;
2526
isLatestTodoBlock?: boolean;
@@ -65,8 +66,7 @@ function todosToMarkdown(todos: TodoItem[]): string {
6566
* Determine the dot color based on tool execution status
6667
*/
6768
function getToolStatus(
68-
block: Block,
69-
isAwaitingFileApproval?: boolean
69+
block: Block
7070
): 'loading' | 'content' | 'success' | 'failure' | 'mixed' | 'pending' {
7171
if (block.loading) {
7272
return 'loading';
@@ -78,7 +78,11 @@ function getToolStatus(
7878
const hasTools = toolCalls.length > 0;
7979

8080
if (hasTools) {
81-
if (isAwaitingFileApproval) {
81+
// Check if any tool has pending approval or pending question
82+
const hasPending = toolLinks.some(
83+
link => link?.params?.pending_approval || link?.params?.pending_question
84+
);
85+
if (hasPending) {
8286
return 'pending';
8387
}
8488

@@ -120,6 +124,7 @@ function BlockComponent({
120124
block,
121125
blockIndex: _blockIndex,
122126
isAwaitingFileApproval,
127+
isAwaitingQuestion,
123128
isLast,
124129
isLatestTodoBlock,
125130
isFocused,
@@ -291,7 +296,8 @@ function BlockComponent({
291296
}
292297
};
293298

294-
const showActions = isFocused && !block.loading && !isAwaitingFileApproval;
299+
const showActions =
300+
isFocused && !block.loading && !isAwaitingFileApproval && !isAwaitingQuestion;
295301

296302
return (
297303
<Block
@@ -316,7 +322,7 @@ function BlockComponent({
316322
) : (
317323
<BlockRow>
318324
<ResponseDot
319-
status={getToolStatus(block, isAwaitingFileApproval)}
325+
status={getToolStatus(block)}
320326
hasOnlyTools={!hasContent && hasTools}
321327
/>
322328
<BlockContentWrapper hasOnlyTools={!hasContent && hasTools}>
@@ -433,7 +439,9 @@ export default BlockComponent;
433439

434440
const Block = styled('div')<{isFocused?: boolean; isLast?: boolean}>`
435441
width: 100%;
436-
border-bottom: ${p => (p.isLast ? 'none' : `1px solid ${p.theme.border}`)};
442+
border-top: 1px solid transparent;
443+
border-bottom: ${p =>
444+
p.isLast ? '1px solid transparent' : `1px solid ${p.theme.border}`};
437445
position: relative;
438446
flex-shrink: 0; /* Prevent blocks from shrinking */
439447
cursor: pointer;

0 commit comments

Comments
 (0)