import { useState, useCallback, useMemo } from "react"; import { View, Text, TextInput, Pressable, ActivityIndicator, type PressableStateCallbackType, } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; import { Check, CircleHelp, X } from "lucide-react-native"; import type { PendingPermission } from "@/types/shared"; import type { AgentPermissionResponse } from "@getpaseo/protocol/agent-types"; import { isWeb } from "@/constants/platform"; import { areQuestionsAnswered, buildQuestionFormAnswers, parseQuestionFormQuestions, questionShowsTextInput, resolveDismissLabel, shouldSubmitEmptyOnDismiss, type QuestionFormQuestion, type QuestionOption, } from "./question-form-card-core"; interface QuestionFormCardProps { permission: PendingPermission; onRespond: (response: AgentPermissionResponse) => void; isResponding: boolean; } const IS_WEB = isWeb; function getQuestionInputPlaceholder(question: QuestionFormQuestion): string { return ( question.placeholder ?? (question.options.length === 0 ? "Type your answer..." : "Other...") ); } interface QuestionOptionRowProps { qIndex: number; optIndex: number; option: QuestionOption; isSelected: boolean; multiSelect: boolean; isResponding: boolean; onToggle: (qIndex: number, optIndex: number, multiSelect: boolean) => void; } function QuestionOptionRow({ qIndex, optIndex, option, isSelected, multiSelect, isResponding, onToggle, }: QuestionOptionRowProps) { const { theme } = useUnistyles(); const handlePress = useCallback(() => { onToggle(qIndex, optIndex, multiSelect); }, [onToggle, qIndex, optIndex, multiSelect]); const pressableStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.optionItem, (Boolean(hovered) || isSelected) && { backgroundColor: theme.colors.surface2, }, pressed && styles.optionItemPressed, ], [isSelected, theme.colors.surface2], ); const optionLabelStyle = useMemo( () => [styles.optionLabel, { color: theme.colors.foreground }], [theme.colors.foreground], ); const optionDescriptionStyle = useMemo( () => [styles.optionDescription, { color: theme.colors.foregroundMuted }], [theme.colors.foregroundMuted], ); return ( {option.label} {option.description ? ( {option.description} ) : null} {isSelected ? ( ) : null} ); } interface QuestionOtherInputProps { qIndex: number; value: string; placeholder: string; isResponding: boolean; onChange: (qIndex: number, text: string) => void; onSubmit: () => void; } function QuestionOtherInput({ qIndex, value, placeholder, isResponding, onChange, onSubmit, }: QuestionOtherInputProps) { const { theme } = useUnistyles(); const handleChange = useCallback( (text: string) => { onChange(qIndex, text); }, [onChange, qIndex], ); const otherInputStyle = useMemo( () => [ styles.otherInput, { borderColor: value.length > 0 ? theme.colors.borderAccent : theme.colors.border, color: theme.colors.foreground, backgroundColor: theme.colors.surface2, }, IS_WEB ? { outlineStyle: "none", outlineWidth: 0, outlineColor: "transparent" } : null, ] as const, [ value.length, theme.colors.borderAccent, theme.colors.border, theme.colors.foreground, theme.colors.surface2, ], ); return ( ); } export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) { const { theme } = useUnistyles(); const isMobile = useIsCompactFormFactor(); const questions = parseQuestionFormQuestions(permission.request.input); const [selections, setSelections] = useState>>({}); const [otherTexts, setOtherTexts] = useState>({}); const [respondingAction, setRespondingAction] = useState<"submit" | "dismiss" | null>(null); const toggleOption = useCallback((qIndex: number, optIndex: number, multiSelect: boolean) => { setSelections((prev) => { const current = prev[qIndex] ?? new Set(); const next = new Set(current); if (multiSelect) { if (next.has(optIndex)) { next.delete(optIndex); } else { next.add(optIndex); } } else { if (next.has(optIndex)) { next.clear(); } else { next.clear(); next.add(optIndex); } } return { ...prev, [qIndex]: next }; }); setOtherTexts((prev) => { if (!prev[qIndex]) return prev; const next = { ...prev }; delete next[qIndex]; return next; }); }, []); const setOtherText = useCallback((qIndex: number, text: string) => { setOtherTexts((prev) => ({ ...prev, [qIndex]: text })); if (text.length > 0) { setSelections((prev) => { if (!prev[qIndex] || prev[qIndex].size === 0) return prev; return { ...prev, [qIndex]: new Set() }; }); } }, []); const allAnswered = areQuestionsAnswered(questions, selections, otherTexts); const handleSubmit = useCallback(() => { if (!questions || !allAnswered || isResponding) return; setRespondingAction("submit"); onRespond({ behavior: "allow", updatedInput: { ...permission.request.input, answers: buildQuestionFormAnswers(questions, selections, otherTexts), }, }); }, [ questions, allAnswered, isResponding, selections, otherTexts, onRespond, permission.request.input, ]); const handleDeny = useCallback(() => { if (!questions) return; setRespondingAction("dismiss"); if (shouldSubmitEmptyOnDismiss(questions)) { onRespond({ behavior: "allow", updatedInput: { ...permission.request.input, answers: buildQuestionFormAnswers(questions, selections, otherTexts), }, }); return; } onRespond({ behavior: "deny", message: "Dismissed by user", }); }, [questions, onRespond, otherTexts, permission.request.input, selections]); const dismissButtonStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.actionButton, { backgroundColor: hovered ? theme.colors.surface2 : theme.colors.surface1, borderColor: theme.colors.borderAccent, }, pressed && styles.optionItemPressed, ], [theme.colors.surface2, theme.colors.surface1, theme.colors.borderAccent], ); const submitDisabled = !allAnswered || isResponding; const submitButtonStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.actionButton, { backgroundColor: hovered && !submitDisabled ? theme.colors.surface2 : theme.colors.surface1, borderColor: submitDisabled ? theme.colors.border : theme.colors.borderAccent, opacity: submitDisabled ? 0.5 : 1, }, pressed && !submitDisabled ? styles.optionItemPressed : null, ], [ submitDisabled, theme.colors.surface2, theme.colors.surface1, theme.colors.border, theme.colors.borderAccent, ], ); const containerStyle = useMemo( () => [ styles.container, { backgroundColor: theme.colors.surface1, borderColor: theme.colors.border, }, ], [theme.colors.surface1, theme.colors.border], ); const questionTextStyle = useMemo( () => [styles.questionText, { color: theme.colors.foreground }], [theme.colors.foreground], ); const actionsContainerStyle = useMemo( () => [styles.actionsContainer, !isMobile && styles.actionsContainerDesktop], [isMobile], ); const dismissActionTextStyle = useMemo( () => [styles.actionText, { color: theme.colors.foregroundMuted }], [theme.colors.foregroundMuted], ); const submitActionTextColor = allAnswered ? theme.colors.foreground : theme.colors.foregroundMuted; const submitActionTextStyle = useMemo( () => [styles.actionText, { color: submitActionTextColor }], [submitActionTextColor], ); if (!questions) { return null; } const dismissLabel = resolveDismissLabel(questions); return ( {questions.map((q, qIndex) => { const selected = selections[qIndex] ?? new Set(); const otherText = otherTexts[qIndex] ?? ""; const showTextInput = questionShowsTextInput(q); return ( {q.question} {q.options.length > 0 ? ( {q.options.map((opt, optIndex) => ( ))} ) : null} {showTextInput ? ( ) : null} ); })} {respondingAction === "dismiss" ? ( ) : ( {dismissLabel} )} {respondingAction === "submit" ? ( ) : ( Submit )} ); } const styles = StyleSheet.create((theme) => ({ container: { padding: theme.spacing[3], borderRadius: theme.spacing[2], borderWidth: 1, gap: theme.spacing[3], }, questionBlock: { gap: theme.spacing[2], }, questionHeader: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingBottom: theme.spacing[1], }, questionText: { flex: 1, fontSize: theme.fontSize.base, lineHeight: 22, }, optionsWrap: { gap: theme.spacing[1], }, optionItem: { flexDirection: "row", alignItems: "center", paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.md, }, optionItemPressed: { opacity: 0.9, }, optionItemContent: { flex: 1, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, optionTextBlock: { flex: 1, gap: 2, }, optionLabel: { fontSize: theme.fontSize.sm, }, optionDescription: { fontSize: theme.fontSize.xs, lineHeight: 16, }, optionCheckSlot: { width: 16, alignItems: "center", justifyContent: "center", marginLeft: "auto", }, otherInput: { borderWidth: 1, borderRadius: theme.borderRadius.lg, paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[3], fontSize: theme.fontSize.sm, }, actionsContainer: { gap: theme.spacing[2], }, actionsContainerDesktop: { flexDirection: "row", justifyContent: "flex-start", alignItems: "center", }, actionButton: { paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[3], borderRadius: theme.borderRadius.md, alignItems: "center", borderWidth: theme.borderWidth[1], }, actionContent: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, actionText: { fontSize: theme.fontSize.sm, }, }));