import { Ionicons } from "@expo/vector-icons"; import { useMemo, useState } from "react"; import { ActivityIndicator, FlatList, Keyboard, KeyboardAvoidingView, Modal, Platform, Pressable, StyleSheet, Text, TextInput, useWindowDimensions, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { colors, monoFont } from "@/lib/theme"; import type { ModelRef, ModelSummary } from "@/lib/types"; type ModelSelectorProps = { models: ModelSummary[]; selected?: ModelRef; loading?: boolean; error?: string | null; disabled?: boolean; onSelect: (model: ModelRef) => void; }; function labelFor(model?: ModelRef, models: ModelSummary[] = []) { if (!model) { return "Select model"; } return models.find((entry) => entry.provider === model.provider && entry.id === model.id)?.id ?? model.id; } export function ModelSelector({ models, selected, loading, error, disabled, onSelect, }: ModelSelectorProps) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const { height: windowHeight } = useWindowDimensions(); const insets = useSafeAreaInsets(); const sheetHeight = Math.round(windowHeight * 0.78); const filteredModels = useMemo(() => { const needle = query.trim().toLowerCase(); if (!needle) { return models; } return models.filter((model) => `${model.id} ${model.name ?? ""} ${model.providerDisplayName ?? model.provider}` .toLowerCase() .includes(needle), ); }, [models, query]); const selectedSummary = selected ? models.find((model) => model.provider === selected.provider && model.id === selected.id) : undefined; const close = () => { Keyboard.dismiss(); setQuery(""); setOpen(false); }; const handleSelect = (model: ModelRef) => { onSelect(model); close(); }; return ( <> [styles.trigger, pressed && styles.pressed, disabled && styles.disabled]} onPress={() => setOpen(true)} disabled={disabled} > {labelFor(selected, models)} {selectedSummary?.providerDisplayName ?? selected?.provider ?? "available models"} {loading ? ( ) : ( )} Choose model {error ? {error} : null} {models.length === 0 ? ( {loading ? "Loading models…" : "No available models"} ) : ( `${model.provider}:${model.id}`} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" contentContainerStyle={ filteredModels.length === 0 ? styles.listEmptyContent : styles.listContent } renderItem={({ item }) => { const isSelected = item.provider === selected?.provider && item.id === selected?.id; return ( [ styles.option, pressed && styles.pressed, isSelected && styles.selected, ]} onPress={() => handleSelect({ provider: item.provider, id: item.id })} > {item.id} {item.providerDisplayName ?? item.provider} {item.name && item.name !== item.id ? ` · ${item.name}` : ""} {isSelected ? ( ) : null} ); }} ListEmptyComponent={No matching models} /> )} ); } const styles = StyleSheet.create({ trigger: { minHeight: 40, flexDirection: "row", alignItems: "center", justifyContent: "space-between", backgroundColor: colors.surfaceDeep, borderRadius: 8, borderWidth: 1, borderColor: colors.border, paddingHorizontal: 11, paddingVertical: 7, }, triggerText: { flex: 1, gap: 1 }, modelId: { color: colors.text, fontFamily: monoFont, fontSize: 13 }, provider: { color: colors.textMuted, fontFamily: monoFont, fontSize: 11 }, pressed: { backgroundColor: colors.surface }, disabled: { opacity: 0.45 }, overlay: { flex: 1, justifyContent: "flex-end", backgroundColor: "rgba(0, 0, 0, 0.65)" }, backdrop: { ...StyleSheet.absoluteFillObject }, sheet: { backgroundColor: colors.surface, borderTopLeftRadius: 16, borderTopRightRadius: 16, borderWidth: 1, borderColor: colors.border, paddingHorizontal: 16, paddingTop: 16, gap: 12, }, header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, title: { color: colors.text, fontSize: 17, fontWeight: "700" }, search: { color: colors.text, backgroundColor: colors.surfaceDeep, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 11, paddingVertical: 9, fontFamily: monoFont, fontSize: 13, }, listRegion: { flex: 1, minHeight: 0, }, list: { flex: 1, }, listContent: { paddingBottom: 8, }, listEmptyContent: { flexGrow: 1, justifyContent: "flex-start", paddingTop: 8, }, option: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: 12, paddingHorizontal: 10, borderRadius: 8, }, selected: { backgroundColor: "rgba(147, 197, 253, 0.12)" }, optionText: { flex: 1, gap: 2 }, optionId: { color: colors.text, fontFamily: monoFont, fontSize: 13 }, optionMeta: { color: colors.textMuted, fontFamily: monoFont, fontSize: 11 }, error: { color: colors.errorMuted, fontSize: 12 }, empty: { color: colors.textMuted, fontFamily: monoFont, fontSize: 13, paddingVertical: 20 }, });