Skip to content
Open
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,33 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.text,
flex: 1,
},
tabBar: {
flexDirection: 'row',
gap: 2,
borderBottomWidth: 1,
borderBottomColor: theme.colors.divider,
},
tab: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 12,
paddingVertical: 8,
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
tabActive: {
borderBottomColor: theme.colors.button.primary.background,
},
tabText: {
fontSize: 13,
fontWeight: '500',
color: theme.colors.textSecondary,
},
tabTextActive: {
color: theme.colors.text,
fontWeight: '600',
},
}));

export const AskUserQuestionView = React.memo<ToolViewProps>(({ tool, sessionId, interaction }) => {
Expand All @@ -210,6 +237,7 @@ export const AskUserQuestionView = React.memo<ToolViewProps>(({ tool, sessionId,
const [freeformAnswers, setFreeformAnswers] = React.useState<Map<number, string>>(new Map());
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [isSubmitted, setIsSubmitted] = React.useState(false);
const [activeTab, setActiveTab] = React.useState(0);

// Parse input
const input = tool.input as AskUserQuestionInput | undefined;
Expand All @@ -229,6 +257,8 @@ export const AskUserQuestionView = React.memo<ToolViewProps>(({ tool, sessionId,
? t('session.sharing.permissionApprovalsDisabledReadOnly')
: t('session.sharing.permissionApprovalsDisabledNotGranted');

const showTabs = questions.length > 1;

// Check if all questions have at least one selection
const allQuestionsAnswered = questions.every((_, qIndex) => {
const q = questions[qIndex];
Expand All @@ -241,6 +271,18 @@ export const AskUserQuestionView = React.memo<ToolViewProps>(({ tool, sessionId,
return Boolean(selected && selected.size > 0);
});

// Helper: check if a specific question is answered
const isQuestionAnswered = React.useCallback((qIndex: number): boolean => {
const q = questions[qIndex];
const options = Array.isArray(q?.options) ? q.options : [];
if (options.length === 0) {
const value = freeformAnswers.get(qIndex);
return typeof value === 'string' && value.trim().length > 0;
}
const selected = selections.get(qIndex);
return Boolean(selected && selected.size > 0);
}, [questions, selections, freeformAnswers]);

const handleOptionToggle = React.useCallback((questionIndex: number, optionIndex: number, multiSelect: boolean) => {
if (!canInteract) return;

Expand Down Expand Up @@ -370,6 +412,89 @@ export const AskUserQuestionView = React.memo<ToolViewProps>(({ tool, sessionId,
);
}

const renderQuestionContent = (question: Question, qIndex: number) => {
const selectedOptions = selections.get(qIndex) || new Set();
const options = Array.isArray(question.options) ? question.options : [];

return (
<View key={qIndex} style={styles.questionSection}>
{!showTabs && (
<View style={styles.headerChip}>
<Text style={styles.headerText}>{question.header}</Text>
</View>
)}
<Text style={styles.questionText}>{question.question}</Text>
<View style={styles.optionsContainer}>
{options.length === 0 ? (
<View>
<TextInput
style={styles.freeformInput}
value={freeformAnswers.get(qIndex) ?? ''}
onChangeText={(text) => {
if (!canInteract) return;
setFreeformAnswers((prev) => {
const next = new Map(prev);
next.set(qIndex, text);
return next;
});
}}
placeholder={question.freeform?.placeholder ?? t('tools.askUserQuestion.otherPlaceholder')}
placeholderTextColor={theme.colors.textSecondary}
editable={canInteract}
autoCapitalize="none"
autoCorrect={false}
/>
{question.freeform?.description ? (
<Text style={styles.freeformDescription}>{question.freeform.description}</Text>
) : null}
</View>
) : null}
{options.map((option, oIndex) => {
const isSelected = selectedOptions.has(oIndex);

return (
<TouchableOpacity
key={oIndex}
style={[
styles.optionButton,
isSelected && styles.optionButtonSelected,
!canInteract && styles.optionButtonDisabled,
]}
onPress={() => handleOptionToggle(qIndex, oIndex, question.multiSelect)}
disabled={!canInteract}
activeOpacity={0.7}
>
{question.multiSelect ? (
<View style={[
styles.checkboxOuter,
isSelected && styles.checkboxOuterSelected,
]}>
{isSelected && (
<Ionicons name="checkmark" size={14} color={theme.colors.button.primary.tint} />
)}
</View>
) : (
<View style={[
styles.radioOuter,
isSelected && styles.radioOuterSelected,
]}>
{isSelected && <View style={styles.radioInner} />}
</View>
)}
<View style={styles.optionContent}>
<Text style={styles.optionLabel}>{option.label}</Text>
{option.description && (
<Text style={styles.optionDescription}>{option.description}</Text>
)}
</View>
</TouchableOpacity>
);
})}
</View>
</View>
);
};

return (
<ToolSectionView>
<View style={styles.container}>
Expand All @@ -378,86 +503,39 @@ export const AskUserQuestionView = React.memo<ToolViewProps>(({ tool, sessionId,
{disabledMessage}
</Text>
) : null}
{questions.map((question, qIndex) => {
const selectedOptions = selections.get(qIndex) || new Set();
const options = Array.isArray(question.options) ? question.options : [];

return (
<View key={qIndex} style={styles.questionSection}>
<View style={styles.headerChip}>
<Text style={styles.headerText}>{question.header}</Text>
</View>
<Text style={styles.questionText}>{question.question}</Text>
<View style={styles.optionsContainer}>
{options.length === 0 ? (
<View>
<TextInput
style={styles.freeformInput}
value={freeformAnswers.get(qIndex) ?? ''}
onChangeText={(text) => {
if (!canInteract) return;
setFreeformAnswers((prev) => {
const next = new Map(prev);
next.set(qIndex, text);
return next;
});
}}
placeholder={question.freeform?.placeholder ?? t('tools.askUserQuestion.otherPlaceholder')}
placeholderTextColor={theme.colors.textSecondary}
editable={canInteract}
autoCapitalize="none"
autoCorrect={false}
/>
{question.freeform?.description ? (
<Text style={styles.freeformDescription}>{question.freeform.description}</Text>
) : null}
</View>
) : null}
{options.map((option, oIndex) => {
const isSelected = selectedOptions.has(oIndex);

return (
<TouchableOpacity
key={oIndex}
style={[
styles.optionButton,
isSelected && styles.optionButtonSelected,
!canInteract && styles.optionButtonDisabled,
]}
onPress={() => handleOptionToggle(qIndex, oIndex, question.multiSelect)}
disabled={!canInteract}
activeOpacity={0.7}
>
{question.multiSelect ? (
<View style={[
styles.checkboxOuter,
isSelected && styles.checkboxOuterSelected,
]}>
{isSelected && (
<Ionicons name="checkmark" size={14} color={theme.colors.button.primary.tint} />
)}
</View>
) : (
<View style={[
styles.radioOuter,
isSelected && styles.radioOuterSelected,
]}>
{isSelected && <View style={styles.radioInner} />}
</View>
)}
<View style={styles.optionContent}>
<Text style={styles.optionLabel}>{option.label}</Text>
{option.description && (
<Text style={styles.optionDescription}>{option.description}</Text>
)}
</View>
</TouchableOpacity>
);
})}
</View>

{showTabs ? (
<>
<View style={styles.tabBar}>
{questions.map((q, qIndex) => {
const isActive = qIndex === activeTab;
const answered = isQuestionAnswered(qIndex);
return (
<TouchableOpacity
key={qIndex}
style={[styles.tab, isActive && styles.tabActive]}
onPress={() => setActiveTab(qIndex)}
activeOpacity={0.7}
>
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>
{q.header}
</Text>
{answered && (
<Ionicons
name="checkmark-circle"
size={14}
color={isActive ? theme.colors.button.primary.background : theme.colors.textSecondary}
/>
)}
</TouchableOpacity>
);
})}
</View>
);
})}
{renderQuestionContent(questions[activeTab]!, activeTab)}
</>
) : (
renderQuestionContent(questions[0]!, 0)
)}

{canInteract && (
<View style={styles.actionsContainer}>
Expand Down