juq-llm-kit
Version:
Customizable UI components for React Native (Expo) chat applications
884 lines (840 loc) • 25.9 kB
JavaScript
#!/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', '')}';`);
}
}