juq-llm-kit
Version:
Customizable UI components for React Native (Expo) chat applications
403 lines (401 loc) • 14.9 kB
JavaScript
;
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;