UNPKG

juq-llm-kit

Version:

Customizable UI components for React Native (Expo) chat applications

884 lines (840 loc) 25.9 kB
#!/usr/bin/env node "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 }; }; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); const child_process_1 = require("child_process"); // This function provides fallback content for component files if they can't be found const getInlineFileContent = (fileName) => { const templates = { 'chat-input.tsx': `import React, { useState } from 'react'; import { View, TextInput, TouchableOpacity, StyleSheet, Text } from 'react-native'; interface ChatInputProps { onSubmit: (message: string) => void; placeholder?: string; isLoading?: boolean; theme?: 'light' | 'dark'; } const ChatInput: React.FC<ChatInputProps> = ({ onSubmit, placeholder = 'Type a message...', isLoading = false, theme = 'dark', }) => { const [message, setMessage] = useState(''); const handleSubmit = () => { if (message.trim() && !isLoading) { onSubmit(message.trim()); setMessage(''); } }; const isDark = theme === 'dark'; return ( <View style={[ styles.container, isDark ? styles.containerDark : styles.containerLight ]}> <TextInput style={[ styles.input, isDark ? styles.inputDark : styles.inputLight ]} value={message} onChangeText={setMessage} placeholder={placeholder} placeholderTextColor={isDark ? '#888' : '#999'} multiline editable={!isLoading} /> <TouchableOpacity style={[ styles.button, isDark ? styles.buttonDark : styles.buttonLight, isLoading ? styles.buttonDisabled : null ]} onPress={handleSubmit} disabled={isLoading} > <Text style={styles.buttonText}> {isLoading ? 'Loading...' : 'Send'} </Text> </TouchableOpacity> </View> ); }; const styles = StyleSheet.create({ container: { flexDirection: 'row', padding: 10, alignItems: 'center', borderTopWidth: 1, }, containerDark: { backgroundColor: '#1a1a1a', borderTopColor: '#333', }, containerLight: { backgroundColor: '#fff', borderTopColor: '#e0e0e0', }, input: { flex: 1, padding: 10, borderRadius: 20, maxHeight: 100, }, inputDark: { backgroundColor: '#333', color: 'white', }, inputLight: { backgroundColor: '#f0f0f0', color: 'black', }, button: { marginLeft: 10, padding: 10, borderRadius: 20, justifyContent: 'center', alignItems: 'center', }, buttonDark: { backgroundColor: '#4A90E2', }, buttonLight: { backgroundColor: '#007AFF', }, buttonDisabled: { opacity: 0.5, }, buttonText: { color: 'white', fontWeight: 'bold', }, }); export default ChatInput;`, 'loading-text-animation.tsx': `import React, { useEffect, useState } from 'react'; import { Text, StyleSheet, View } from 'react-native'; interface LoadingTextAnimationProps { phrases?: string[]; animationDuration?: number; theme?: 'light' | 'dark'; } const LoadingTextAnimation: React.FC<LoadingTextAnimationProps> = ({ phrases = ['Loading...', 'Thinking...', 'Processing...'], animationDuration = 3000, theme = 'dark', }) => { const [currentPhraseIndex, setCurrentPhraseIndex] = useState(0); const [dots, setDots] = useState(''); useEffect(() => { const phraseInterval = setInterval(() => { setCurrentPhraseIndex((prevIndex) => (prevIndex + 1) % phrases.length); }, animationDuration); return () => clearInterval(phraseInterval); }, [phrases, animationDuration]); useEffect(() => { const dotsInterval = setInterval(() => { setDots((prevDots) => { if (prevDots.length >= 3) return ''; return prevDots + '.'; }); }, 500); return () => clearInterval(dotsInterval); }, []); const isDark = theme === 'dark'; return ( <View style={styles.container}> <Text style={[ styles.text, isDark ? styles.textDark : styles.textLight ]}> {phrases[currentPhraseIndex]}{dots} </Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 10, alignItems: 'center', }, text: { fontSize: 16, }, textDark: { color: '#888', }, textLight: { color: '#555', }, }); export default LoadingTextAnimation;`, 'messages.tsx': `import React from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; interface Message { id: string | number; content: string; role: 'user' | 'assistant' | 'system'; timestamp?: string; } interface MessagesProps { messages: Message[]; onCopy?: (text: string) => void; theme?: 'light' | 'dark'; } const Messages: React.FC<MessagesProps> = ({ messages, onCopy, theme = 'dark', }) => { const copyToClipboard = (text: string) => { if (onCopy) { onCopy(text); } }; const isDark = theme === 'dark'; return ( <ScrollView style={[ styles.container, isDark ? styles.containerDark : styles.containerLight ]}> {messages.map((message) => ( <View key={message.id} style={[ styles.messageContainer, message.role === 'user' ? styles.userMessageContainer : styles.aiMessageContainer, ]} > <View style={[ styles.messageBubble, message.role === 'user' ? (isDark ? styles.userBubbleDark : styles.userBubbleLight) : (isDark ? styles.aiBubbleDark : styles.aiBubbleLight) ]}> <Text style={[ styles.messageText, isDark ? styles.textDark : styles.textLight ]}> {message.content} </Text> {message.role === 'assistant' && ( <TouchableOpacity style={styles.copyButton} onPress={() => copyToClipboard(message.content)} > <Text style={styles.copyButtonText}>Copy</Text> </TouchableOpacity> )} </View> {message.timestamp && ( <Text style={styles.timestamp}> {new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} </Text> )} </View> ))} </ScrollView> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, containerDark: { backgroundColor: '#121212', }, containerLight: { backgroundColor: '#f5f5f5', }, messageContainer: { padding: 10, marginVertical: 5, }, userMessageContainer: { alignItems: 'flex-end', }, aiMessageContainer: { alignItems: 'flex-start', }, messageBubble: { padding: 12, borderRadius: 18, maxWidth: '80%', }, userBubbleDark: { backgroundColor: '#4A90E2', }, userBubbleLight: { backgroundColor: '#007AFF', }, aiBubbleDark: { backgroundColor: '#333', }, aiBubbleLight: { backgroundColor: '#e0e0e0', }, messageText: { fontSize: 16, }, textDark: { color: 'white', }, textLight: { color: 'black', }, timestamp: { fontSize: 12, color: '#888', marginTop: 4, }, copyButton: { marginTop: 8, alignSelf: 'flex-end', }, copyButtonText: { color: '#4A90E2', fontSize: 14, }, }); export default Messages;`, 'side-bar.tsx': `import React, { useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'; interface Project { name: string; id?: string; onSelect?: () => void; } interface SidebarProps { onCollapsedChange?: (collapsed: boolean) => void; initialCollapsed?: boolean; projects?: Project[]; theme?: 'light' | 'dark'; } const Sidebar: React.FC<SidebarProps> = ({ onCollapsedChange, initialCollapsed = false, projects = [], theme = 'dark', }) => { const [collapsed, setCollapsed] = useState(initialCollapsed); const toggleCollapsed = () => { const newCollapsed = !collapsed; setCollapsed(newCollapsed); if (onCollapsedChange) { onCollapsedChange(newCollapsed); } }; const isDark = theme === 'dark'; if (collapsed) { return ( <TouchableOpacity style={[ styles.collapsedContainer, isDark ? styles.containerDark : styles.containerLight ]} onPress={toggleCollapsed} > <Text style={isDark ? styles.iconDark : styles.iconLight}>≫</Text> </TouchableOpacity> ); } return ( <View style={[ styles.container, isDark ? styles.containerDark : styles.containerLight ]}> <View style={styles.header}> <Text style={[ styles.title, isDark ? styles.textDark : styles.textLight ]}> Projects </Text> <TouchableOpacity onPress={toggleCollapsed}> <Text style={isDark ? styles.iconDark : styles.iconLight}>≪</Text> </TouchableOpacity> </View> <ScrollView style={styles.content}> {projects.length > 0 ? ( <View style={styles.section}> {projects.map((project, index) => ( <TouchableOpacity key={project.id || index} style={[ styles.item, isDark ? styles.itemDark : styles.itemLight ]} onPress={project.onSelect} > <Text style={[ styles.itemText, isDark ? styles.textDark : styles.textLight ]}> {project.name} </Text> </TouchableOpacity> ))} </View> ) : ( <Text style={[ styles.emptyText, isDark ? styles.textDark : styles.textLight ]}> No projects yet </Text> )} </ScrollView> </View> ); }; const styles = StyleSheet.create({ container: { width: 250, height: '100%', borderRightWidth: 1, }, containerDark: { backgroundColor: '#1a1a1a', borderRightColor: '#333', }, containerLight: { backgroundColor: '#f5f5f5', borderRightColor: '#e0e0e0', }, collapsedContainer: { width: 40, height: '100%', justifyContent: 'center', alignItems: 'center', borderRightWidth: 1, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, borderBottomWidth: 1, }, title: { fontSize: 18, fontWeight: 'bold', }, content: { flex: 1, }, section: { marginBottom: 20, }, item: { padding: 12, borderBottomWidth: 1, }, itemDark: { borderBottomColor: '#333', }, itemLight: { borderBottomColor: '#e0e0e0', }, itemText: { fontSize: 16, }, textDark: { color: 'white', }, textLight: { color: 'black', }, iconDark: { color: 'white', fontSize: 18, }, iconLight: { color: 'black', fontSize: 18, }, emptyText: { padding: 16, fontStyle: 'italic', textAlign: 'center', }, }); export default Sidebar;`, 'types.ts': `// Message types export interface Message { id: string | number; content: string; role: 'user' | 'assistant' | 'system'; timestamp?: string; } // Theme types export type Theme = 'light' | 'dark'; // Chat input types export interface ChatInputProps { onSubmit: (message: string) => void; placeholder?: string; initialValue?: string; isLoading?: boolean; loadingPhrases?: string[]; maxHeight?: number; categoryButtons?: CategoryButton[]; containerStyle?: object; inputStyle?: object; fontFamily?: string; theme?: Theme; } export interface CategoryButton { icon?: React.ReactNode; label: string; onPress: () => void; } // Messages component types export interface MessagesProps { messages: Message[]; onCopy?: (text: string) => void; onRegenerate?: (messageId: string | number) => void; containerStyle?: object; bubbleStyle?: object; messageTextStyle?: object; fontFamily?: string; theme?: Theme; customActions?: MessageAction[]; } export interface MessageAction { icon: React.ReactNode; onPress: (messageId: string | number) => void; } // Loading animation types export interface LoadingTextAnimationProps { phrases?: string[]; animationDuration?: number; textStyle?: object; containerStyle?: object; fontFamily?: string; theme?: Theme; } // Sidebar types export interface SidebarProps { onCollapsedChange?: (collapsed: boolean) => void; initialCollapsed?: boolean; projects?: Project[]; historyItems?: HistoryItem[]; subscriptionTitle?: string; subscriptionText?: string; containerStyle?: object; theme?: Theme; } export interface Project { name: string; id?: string; onSelect?: () => void; } export interface HistoryItem { name: string; date?: Date; id: string; onSelect?: () => void; }` }; return templates[fileName] || null; }; // Define all components that can be added const components = { 'chat-input': { file: 'components/chat-kit/chat-input.tsx', dependencies: ['react-native-reanimated', 'lucide-react'], additionalFiles: ['types.ts'] }, 'loading-text-animation': { file: 'components/chat-kit/loading-text-animation.tsx', dependencies: ['moti', 'framer-motion'], additionalFiles: ['types.ts'] }, 'messages': { file: 'components/chat-kit/messages.tsx', dependencies: ['expo-clipboard'], additionalFiles: ['types.ts'] }, 'sidebar': { file: 'components/chat-kit/side-bar.tsx', dependencies: ['lucide-react'], additionalFiles: ['types.ts'] }, 'all-chat-kit': { file: '', dependencies: [ 'react-native-reanimated', 'lucide-react', 'moti', 'framer-motion', 'expo-clipboard' ], additionalFiles: [ 'components/chat-kit/chat-input.tsx', 'components/chat-kit/loading-text-animation.tsx', 'components/chat-kit/messages.tsx', 'components/chat-kit/side-bar.tsx', 'types.ts' ] } }; const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .command('add <component>', 'Add a component to your project', (yargs) => { yargs.positional('component', { describe: 'Component to add', type: 'string', choices: Object.keys(components), }); yargs.option('dir', { describe: 'Target directory within your project', type: 'string', default: 'components' }); }) .command('list', 'List all available components') .demandCommand(1, 'You need to specify a command (e.g., "add" or "list")') .help() .parseSync(); if (argv._[0] === 'list') { console.log('Available components:'); Object.keys(components).forEach(name => { console.log(`- ${name}`); }); process.exit(0); } // Debug function to help diagnose source file location issues const debugPaths = (filePath) => { const paths = [ path.join(__dirname, '../src', filePath), path.join(__dirname, '../dist/src', filePath), path.join(__dirname, '../', filePath), path.join(__dirname, '../dist', filePath), path.join(__dirname, '..', filePath), path.join(__dirname, '../src/components', filePath), path.join(__dirname, '../dist/src/components', filePath) ]; console.log(`Debugging paths for ${filePath}:`); paths.forEach(p => { console.log(`- ${p}: ${fs.existsSync(p) ? 'EXISTS' : 'NOT FOUND'}`); }); }; // Helper function to get the source path const getSourcePath = (filePath) => { // Try multiple possible locations for the file const possiblePaths = [ // Source directories (for development) path.join(__dirname, '../src', filePath), path.join(__dirname, '../src/components', filePath), // Dist directories (for installed package) path.join(__dirname, '../dist/src', filePath), path.join(__dirname, '../dist/src/components', filePath), // Root directories (for flat structure) path.join(__dirname, '../', filePath), path.join(__dirname, '../dist', filePath), // Direct paths path.join(__dirname, '..', filePath), // Special case for components directory path.join(__dirname, '../src', filePath.replace('components/', '')), path.join(__dirname, '../dist/src', filePath.replace('components/', '')), ]; // Find the first path that exists for (const checkPath of possiblePaths) { if (fs.existsSync(checkPath)) { return checkPath; } } // If we get here, none of the paths exist debugPaths(filePath); // Check for raw TypeScript file (non-compiled) const rawTsFile = filePath.replace('.tsx', '.ts').replace('.ts', '.tsx'); for (const ext of ['.tsx', '.ts']) { const rawPath = path.join(__dirname, '../src', rawTsFile.replace(/\.(tsx|ts)$/, ext)); if (fs.existsSync(rawPath)) { console.log(`Found raw TypeScript file at: ${rawPath}`); return rawPath; } } // Final attempt: try to create the file from source const sourceFileName = path.basename(filePath); const inlineContent = getInlineFileContent(sourceFileName); if (inlineContent) { // Create a temporary file const tempDir = path.join(__dirname, '../temp'); fs.ensureDirSync(tempDir); const tempFile = path.join(tempDir, sourceFileName); fs.writeFileSync(tempFile, inlineContent); console.log(`Created file from inline source: ${tempFile}`); return tempFile; } console.error(`Could not find file: ${filePath}`); console.error(`Checked locations: - ${__dirname}/../src/${filePath} - ${__dirname}/../dist/src/${filePath} - ${__dirname}/../${filePath} - ${__dirname}/../dist/${filePath} - ${__dirname}/../src/components/${filePath} - ${__dirname}/../dist/src/components/${filePath} `); // Return the most likely path as a fallback return path.join(__dirname, '../src', filePath); }; if (argv._[0] === 'add' && argv.component) { const component = argv.component; const componentInfo = components[component]; const targetBaseDir = path.join(process.cwd(), argv.dir || 'components'); // Ensure the target directory exists fs.ensureDirSync(targetBaseDir); // Special case for 'all-chat-kit' which adds all components if (component === 'all-chat-kit') { const chatKitDir = path.join(targetBaseDir, 'chat-kit'); fs.ensureDirSync(chatKitDir); // Copy all chat kit components try { (_a = componentInfo.additionalFiles) === null || _a === void 0 ? void 0 : _a.forEach(file => { const isTypeFile = file === 'types.ts'; const sourcePath = getSourcePath(file); // Determine target path based on file type const targetPath = isTypeFile ? path.join(targetBaseDir, '..', 'types.ts') : path.join(targetBaseDir, path.basename(path.dirname(file)), path.basename(file)); // Ensure parent directory exists fs.ensureDirSync(path.dirname(targetPath)); // Copy the file fs.copySync(sourcePath, targetPath); console.log(`Added ${file} to ${targetPath}`); }); console.log(`All chat kit components added to ${chatKitDir}`); } catch (err) { console.error(`Error copying components:`, err); console.error(`Make sure the package is correctly installed and built.`); process.exit(1); } } else { // Handle individual component const componentFileName = path.basename(componentInfo.file); // Extract folder structure, but remove the first 'components/' part for target const componentDirFull = path.dirname(componentInfo.file); const componentDir = componentDirFull.replace(/^components\//, ''); const sourcePath = getSourcePath(componentInfo.file); const targetDir = path.join(targetBaseDir, componentDir); const targetPath = path.join(targetDir, componentFileName); // Ensure the target directory exists fs.ensureDirSync(targetDir); // Copy the component file try { // Debug information console.log(`Copying component file:`); console.log(`- Source: ${sourcePath}`); console.log(`- Target: ${targetPath}`); fs.copySync(sourcePath, targetPath); console.log(`${component} added to ${targetPath}`); // Copy additional files if needed if ((_b = componentInfo.additionalFiles) === null || _b === void 0 ? void 0 : _b.length) { componentInfo.additionalFiles.forEach(file => { const sourcePath = getSourcePath(file); const targetPath = path.join(targetBaseDir, '..', file); // Ensure parent directory exists fs.ensureDirSync(path.dirname(targetPath)); // Copy if doesn't exist or overwrite with confirmation if (!fs.existsSync(targetPath)) { fs.copySync(sourcePath, targetPath); console.log(`Added ${file} to ${targetPath}`); } else { console.log(`File ${targetPath} already exists. Skipping.`); } }); } } catch (err) { console.error(`Error copying ${component}:`, err); console.error(`Make sure the package is correctly installed and built.`); console.error(`If you continue to have issues, try installing from the source repository:`); console.error(` git clone https://github.com/Jukka-Sun/jus-llm-kit.git`); console.error(` cd juq-llm-kit`); console.error(` npm install`); console.error(` npm run build`); console.error(` npm link`); process.exit(1); } } // Install dependencies in the target project try { if (componentInfo.dependencies.length > 0) { console.log('Installing required dependencies...'); const dependencies = componentInfo.dependencies.join(' '); // Check if using yarn or npm const hasYarnLock = fs.existsSync(path.join(process.cwd(), 'yarn.lock')); if (hasYarnLock) { (0, child_process_1.execSync)(`yarn add ${dependencies}`, { stdio: 'inherit' }); } else { (0, child_process_1.execSync)(`npm install ${dependencies}`, { stdio: 'inherit' }); } console.log('Dependencies installed successfully.'); } } catch (err) { console.error('Error installing dependencies:', err); process.exit(1); } console.log('\nSetup complete! You can now import and use the component in your project.'); console.log('\nImport example:'); if (component === 'all-chat-kit') { console.log(` import ChatInput from './${argv.dir || 'components'}/chat-kit/chat-input'; import LoadingTextAnimation from './${argv.dir || 'components'}/chat-kit/loading-text-animation'; import Messages from './${argv.dir || 'components'}/chat-kit/messages'; import Sidebar from './${argv.dir || 'components'}/chat-kit/side-bar'; `); } else { const componentName = component.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(''); console.log(`import ${componentName} from './${argv.dir || 'components'}/${componentInfo.file.replace('.tsx', '')}';`); } }