UNPKG

juq-llm-kit

Version:

Customizable UI components for React Native (Expo) chat applications

403 lines (401 loc) 14.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = __importStar(require("react")); const react_native_1 = require("react-native"); const lucide_react_1 = require("lucide-react"); const loading_text_animation_1 = __importDefault(require("./loading-text-animation")); // Web-specific styles using type assertion const webInputStyles = react_native_1.Platform.select({ web: { outlineStyle: 'none', resize: 'none', // Add styles to hide scrollbar on web scrollbarWidth: 'none', // Firefox msOverflowStyle: 'none', // IE and Edge }, default: {} }); // Default category buttons const defaultCategoryButtons = [ { icon: <lucide_react_1.FileText size={20} color="#E5E7EB"/>, label: "Summary" }, { icon: <lucide_react_1.Code size={20} color="#E5E7EB"/>, label: "Code" }, { icon: <lucide_react_1.Pen size={20} color="#E5E7EB"/>, label: "Design" }, { icon: <lucide_react_1.Search size={20} color="#E5E7EB"/>, label: "Research" } ]; const ChatInput = ({ placeholder = "Ask anything...", initialValue = "", onSubmit, isLoading = false, loadingPhrases, maxHeight = 120, categoryButtons = defaultCategoryButtons, containerStyle, inputStyle, fontFamily = "SpaceMono", theme = "dark" }) => { const [inputValue, setInputValue] = (0, react_1.useState)(initialValue); const [isExpanded, setIsExpanded] = (0, react_1.useState)(false); const [textHeight, setTextHeight] = (0, react_1.useState)(0); const [showResizeButton, setShowResizeButton] = (0, react_1.useState)(false); const [isPortrait, setIsPortrait] = (0, react_1.useState)(false); const expandAnim = new react_native_1.Animated.Value(0); const inputRef = (0, react_1.useRef)(null); // Define scroll container style with custom max height const scrollContainerStyle = { flex: 1, minHeight: 56, maxHeight, }; // Get theme colors const colors = getThemeColors(theme); // Function to handle orientation changes const handleOrientationChange = () => { const { width, height } = react_native_1.Dimensions.get('window'); setIsPortrait(height > width); }; // Set up orientation listener (0, react_1.useEffect)(() => { handleOrientationChange(); // Initial check // Add listener for dimension changes const subscription = react_native_1.Dimensions.addEventListener('change', handleOrientationChange); // Clean up listener return () => { // For RN < 0.65, use subscription.remove() // For RN >= 0.65, use subscription.remove() if (typeof subscription.remove === 'function') { subscription.remove(); } else { // @ts-ignore - handle older versions of React Native react_native_1.Dimensions.removeEventListener('change', handleOrientationChange); } }; }, []); (0, react_1.useEffect)(() => { if (react_native_1.Platform.OS === 'web') { const style = document.createElement('style'); style.textContent = ` .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } `; document.head.appendChild(style); return () => { document.head.removeChild(style); }; } }, []); const handleInputChange = (text) => { setInputValue(text); }; const handleContentSizeChange = (event) => { const height = event.nativeEvent.contentSize.height; setTextHeight(height); setShowResizeButton(height > maxHeight); if (!isExpanded) { setTextHeight(Math.min(height, maxHeight)); } }; const toggleExpand = () => { setIsExpanded(!isExpanded); react_native_1.Animated.spring(expandAnim, { toValue: isExpanded ? 0 : 1, useNativeDriver: true, tension: 20, friction: 7 }).start(); }; const animatedContainerStyle = { transform: [{ scale: expandAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 1.1] }) }] }; const handleSubmit = () => { if (!inputValue.trim() || isLoading) return; if (onSubmit) { onSubmit(inputValue); } setInputValue(''); }; return (<react_native_1.Animated.View style={[ styles.container, getContainerStyles(colors), animatedContainerStyle, isExpanded && styles.expandedContainer, containerStyle ]}> {/* Input area */} <react_native_1.View style={styles.inputContainer}> <react_native_1.View style={[styles.inputWrapper, isExpanded && styles.expandedInputWrapper]}> {isLoading ? (<react_native_1.View style={[styles.loadingContainer, { backgroundColor: colors.inputBg }]}> <loading_text_animation_1.default phrases={loadingPhrases} textStyle={{ color: colors.placeholderColor }} fontFamily={fontFamily}/> </react_native_1.View>) : (<react_native_1.ScrollView style={scrollContainerStyle} showsVerticalScrollIndicator={false} showsHorizontalScrollIndicator={false} scrollEnabled={true} nestedScrollEnabled={true}> <react_native_1.TextInput ref={inputRef} placeholder={placeholder} placeholderTextColor={colors.placeholderColor} style={[ styles.input, { fontFamily, color: colors.textColor, backgroundColor: colors.inputBg }, isExpanded ? styles.expandedInput : { height: textHeight }, react_native_1.Platform.OS === 'web' && Object.assign(Object.assign({}, webInputStyles), { overflow: 'auto' }), inputStyle ]} value={inputValue} onChangeText={handleInputChange} onContentSizeChange={handleContentSizeChange} multiline scrollEnabled={true}/> </react_native_1.ScrollView>)} <react_native_1.View style={styles.buttonGroup}> {showResizeButton && !isLoading && (<react_native_1.Pressable style={[styles.iconButton, { backgroundColor: colors.buttonBg }]} onPress={toggleExpand}> {isExpanded ? <lucide_react_1.Minimize2 color={colors.iconColor} size={20}/> : <lucide_react_1.Maximize2 color={colors.iconColor} size={20}/>} </react_native_1.Pressable>)} <react_native_1.Pressable style={[styles.sendButton, { backgroundColor: colors.buttonBg }]} onPress={handleSubmit}> <react_native_1.View style={styles.sendButtonInner}> <lucide_react_1.ArrowUp color={colors.iconColor} size={24}/> </react_native_1.View> </react_native_1.Pressable> </react_native_1.View> </react_native_1.View> </react_native_1.View> {/* Button row */} <react_native_1.View style={styles.buttonRowContainer}> <react_native_1.View style={styles.buttonRow}> <react_native_1.Pressable style={[styles.perfectCircleButton, { backgroundColor: colors.inputBg, borderColor: colors.borderColor }]}> <react_native_1.View style={styles.perfectCenterContainer}> <lucide_react_1.Plus size={20} color={colors.iconColor}/> </react_native_1.View> </react_native_1.Pressable> {categoryButtons.map((btn, index) => (<CategoryButton key={`category-${index}`} icon={btn.icon} label={btn.label} isPortrait={isPortrait} onPress={btn.onPress} style={btn.style} iconStyle={btn.iconStyle} colors={colors} fontFamily={fontFamily}/>))} </react_native_1.View> </react_native_1.View> </react_native_1.Animated.View>); }; // Category button component const CategoryButton = ({ icon, label, isPortrait, style, iconStyle, onPress, colors, fontFamily = "SpaceMono" }) => { return (<react_native_1.Pressable style={[ styles.categoryButton, { backgroundColor: colors.inputBg, borderColor: colors.borderColor }, isPortrait && styles.categoryButtonPortrait, style ]} onPress={onPress}> <react_native_1.View style={[ styles.iconContainer, isPortrait && styles.iconContainerPortrait, iconStyle ]}> {icon} </react_native_1.View> {!isPortrait && (<react_native_1.Text style={[styles.labelText, { fontFamily, color: colors.textColor }]}>{label}</react_native_1.Text>)} </react_native_1.Pressable>); }; // Theme helper function function getThemeColors(theme) { if (theme === 'light') { return { containerBg: '#f9fafb', inputBg: '#e5e7eb', buttonBg: '#d1d5db', textColor: '#111827', placeholderColor: '#6b7280', iconColor: '#374151', borderColor: '#d1d5db' }; } return { containerBg: '#111827', inputBg: '#1F2937', buttonBg: '#374151', textColor: '#E5E7EB', placeholderColor: '#9CA3AF', iconColor: '#E5E7EB', borderColor: '#374151' }; } // Container styles based on theme function getContainerStyles(colors) { return { backgroundColor: colors.containerBg, borderColor: colors.borderColor, }; } const styles = react_native_1.StyleSheet.create({ container: Object.assign(Object.assign({ width: '100%', maxWidth: 896, borderRadius: 24, borderWidth: 1, overflow: 'hidden', marginBottom: 20 }, react_native_1.Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.25, shadowRadius: 4, }, android: { elevation: 5, }, web: { boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', }, })), { zIndex: 1000 }), expandedContainer: { width: '100%', maxWidth: '100%', height: 'auto', maxHeight: '80%', }, inputContainer: { padding: 16, }, inputWrapper: { position: 'relative', flexDirection: 'row', alignItems: 'flex-start', }, expandedInputWrapper: { minHeight: 120, }, input: Object.assign({ flex: 1, minHeight: 56, maxHeight: react_native_1.Platform.OS === 'web' ? undefined : 120, paddingVertical: 16, paddingHorizontal: 24, paddingRight: 100, fontSize: 18, borderRadius: 9999, textAlignVertical: 'center', lineHeight: 24 }, (react_native_1.Platform.OS === 'web' ? { outline: 'none', overflow: 'auto', } : {})), expandedInput: { height: react_native_1.Platform.OS === 'web' ? '60vh' : '60%', maxHeight: react_native_1.Platform.OS === 'web' ? '60vh' : '60%', }, buttonGroup: { position: 'absolute', right: 8, top: '50%', transform: [{ translateY: -20 }], flexDirection: 'row', gap: 8, alignItems: 'center', }, iconButton: { width: 40, height: 40, borderRadius: 9999, justifyContent: 'center', alignItems: 'center', }, sendButton: { width: 40, height: 40, borderRadius: 9999, overflow: 'hidden', }, sendButtonInner: { flex: 1, justifyContent: 'center', alignItems: 'center', }, buttonRowContainer: { width: '100%', paddingHorizontal: 16, paddingBottom: 16, }, buttonRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-start', alignItems: 'center', }, categoryButton: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 8, paddingHorizontal: 16, borderRadius: 9999, borderWidth: 1, }, categoryButtonPortrait: { width: 40, height: 40, padding: 0, justifyContent: 'center', alignItems: 'center', }, iconContainer: { justifyContent: 'center', alignItems: 'center', }, iconContainerPortrait: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, labelText: { fontSize: 14, }, perfectCircleButton: { width: 40, height: 40, borderRadius: 20, borderWidth: 1, overflow: 'hidden', position: 'relative', }, perfectCenterContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', display: 'flex', }, loadingContainer: { flex: 1, minHeight: 56, borderRadius: 9999, justifyContent: 'center', alignItems: 'flex-start', paddingVertical: 16, }, }); exports.default = ChatInput;