import React, { useState } from 'react'; import { Modal, Pressable, ScrollView, Text, View } from 'react-native'; import { Check, Lock, Sparkles } from 'lucide-react-native'; import { useAgentBi } from '../../analytics/mixpanelContext'; import type { ModelOption } from './modelOptions'; import { showNativeActionSheet } from './nativeActionSheet'; import { styles as sharedStyles } from '../../styles'; import { createThemedStyles, themedColor } from '../../theme'; import type { SuperagentModelChoice } from '../../types'; type OnSelectModel = (input: { agentId: string; model: SuperagentModelChoice | string; }) => Promise | void; const isLockedOption = (option: ModelOption, canSelectBestModel: boolean): boolean => !!option.isBest && !canSelectBestModel; export function useModelPicker({ agentId, selected, options, onSelect, canSelectBestModel = true, onViewPlans, }: { agentId: string; selected: ModelOption; options: ModelOption[]; onSelect?: OnSelectModel; // When false, "best" models stay listed but route to `onViewPlans` instead of selecting. canSelectBestModel?: boolean; onViewPlans?: () => void; }) { const bi = useAgentBi(); const [isOpen, setIsOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const hasLockedModels = options.some((option) => isLockedOption(option, canSelectBestModel)); // Every upgrade entry point fires the (unprefixed, web-matching) event with an // `origin` identifying it; the model selector is `model_selector`. const handleViewPlans = () => { void bi.track('User clicked upgrade', { origin: 'model_selector', cta: 'view_plans' }); onViewPlans?.(); }; const selectModel = (model: ModelOption) => { if (!onSelect || isUpdating) { return; } if (isLockedOption(model, canSelectBestModel)) { setIsOpen(false); handleViewPlans(); return; } // Defense-in-depth: never persist a model that isn't currently in the list. if (!options.some((option) => option.id === model.id)) { return; } setIsOpen(false); if (model.id === selected.id) { return; } // Defer the call so a synchronously-throwing handler is still caught and // isUpdating clears. setIsUpdating(true); Promise.resolve() .then(() => onSelect({ agentId, model: model.id })) .catch(() => undefined) .finally(() => setIsUpdating(false)); }; const openPicker = () => { if (!onSelect || isUpdating) { return; } // The native action sheet can't render the lock/upgrade CTA, so fall back to the // modal when any model is locked; otherwise prefer the native sheet. if (!hasLockedModels) { const didOpenNativePicker = showNativeActionSheet( 'Select model', options.map((model) => ({ label: `${model.id === selected.id ? '✓ ' : ''}${model.label}`, onPress: () => selectModel(model), })), ); if (didOpenNativePicker) { return; } } setIsOpen(true); }; return { isOpen, isUpdating, disabled: !onSelect || isUpdating, open: openPicker, close: () => setIsOpen(false), selectModel, }; } export function ModelPickerModal({ isVisible, options, selectedModelId, canSelectBestModel = true, onClose, onSelect, onViewPlans, }: { isVisible: boolean; options: ModelOption[]; selectedModelId: string; canSelectBestModel?: boolean; onClose: () => void; onSelect: (model: ModelOption) => void; onViewPlans?: () => void; }) { const bi = useAgentBi(); const hasLockedModels = options.some((option) => isLockedOption(option, canSelectBestModel)); const handleViewPlans = () => { void bi.track('User clicked upgrade', { origin: 'model_selector', cta: 'view_plans' }); onViewPlans?.(); }; return ( undefined} style={localStyles.modelModalCard}> Select model {hasLockedModels ? ( [localStyles.builderBadge, pressed && sharedStyles.pressed]} > Builder+ ) : null} {options.map((model) => { const locked = isLockedOption(model, canSelectBestModel); const isSelected = !locked && selectedModelId === model.id; const isAutomatic = model.id === 'default'; return ( onSelect(model)} style={({ pressed }) => [ localStyles.modelModalOption, isSelected && localStyles.modelModalOptionActive, locked && localStyles.modelModalOptionLocked, pressed && sharedStyles.pressed, ]} > {isAutomatic ? ( ) : null} {model.label} {model.isNew ? ( NEW ) : null} {model.description ? ( {model.description} ) : null} {locked ? ( ) : isSelected ? ( ) : null} ); })} {hasLockedModels ? ( [localStyles.viewPlansButton, pressed && sharedStyles.pressed]} > View plans ) : null} ); } const localStyles = createThemedStyles({ autoIcon: { marginRight: 10, }, builderBadge: { backgroundColor: '#F97316', borderRadius: 6, paddingHorizontal: 8, paddingVertical: 3, }, builderBadgeText: { color: '#FFFFFF', fontSize: 11, fontWeight: '800', }, modelDescription: { color: '#A1A1AA', fontSize: 11, fontWeight: '700', lineHeight: 15, marginTop: 4, }, modelLabelRow: { alignItems: 'center', flexDirection: 'row', gap: 8, }, modelModalBackdrop: { backgroundColor: 'rgba(0, 0, 0, 0.58)', flex: 1, justifyContent: 'flex-end', padding: 12, }, modelModalCard: { backgroundColor: '#101012', borderColor: '#2A2A2A', borderRadius: 22, borderWidth: 1, maxHeight: '74%', overflow: 'hidden', }, modelModalHeader: { alignItems: 'center', borderBottomColor: '#242427', borderBottomWidth: 1, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 14, paddingVertical: 12, }, modelModalList: { maxHeight: 430, }, modelModalOption: { alignItems: 'center', borderBottomColor: '#1F1F23', borderBottomWidth: 1, flexDirection: 'row', minHeight: 64, paddingHorizontal: 14, paddingVertical: 10, }, modelModalOptionActive: { backgroundColor: '#1F1F23', }, modelModalOptionLabel: { color: '#F4F4F5', fontSize: 14, fontWeight: '900', }, modelModalOptionLocked: { opacity: 0.55, }, modelModalOptionText: { flex: 1, minWidth: 0, paddingRight: 12, }, modelModalTitle: { color: '#F4F4F5', fontSize: 16, fontWeight: '900', }, newBadge: { backgroundColor: '#2563EB', borderRadius: 5, paddingHorizontal: 6, paddingVertical: 2, }, newBadgeText: { color: '#FFFFFF', fontSize: 9, fontWeight: '800', letterSpacing: 0.4, }, viewPlansButton: { alignItems: 'center', backgroundColor: '#F97316', borderRadius: 12, justifyContent: 'center', marginHorizontal: 14, marginVertical: 12, paddingVertical: 12, }, viewPlansText: { color: '#FFFFFF', fontSize: 14, fontWeight: '800', }, });