feat: Introduce a comprehensive set of AI agent skills, agents, and workflows, along with new UI components and application structure.
This commit is contained in:
1 parent
a2a512d41d
commit
93df87d7e9
317 files changed
+63716
-8727
No files matched your search
@@ -0,0 +1,183 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Send, Square, X } from 'lucide-react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (text: string) => void;
|
||||
onStop?: () => void;
|
||||
isLoading?: boolean;
|
||||
editingText?: string | null;
|
||||
onCancelEdit?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInput({ onSend, onStop, isLoading, editingText, onCancelEdit }: ChatInputProps) {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
// When editingText changes, fill the input
|
||||
useEffect(() => {
|
||||
if (editingText !== null && editingText !== undefined) {
|
||||
setText(editingText);
|
||||
}
|
||||
}, [editingText]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (text.trim() && !isLoading) {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
onSend(text.trim());
|
||||
setText('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
onStop?.();
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setText('');
|
||||
onCancelEdit?.();
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: any) => {
|
||||
if (Platform.OS === 'web') {
|
||||
if (e.nativeEvent.key === 'Enter' && !e.nativeEvent.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isEditing = editingText !== null && editingText !== undefined;
|
||||
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
{/* Edit mode banner */}
|
||||
{isEditing && (
|
||||
<View style={styles.editBanner}>
|
||||
<Text style={styles.editBannerText}>Editing message</Text>
|
||||
<TouchableOpacity onPress={handleCancelEdit} style={styles.editCancelBtn}>
|
||||
<X size={16} color="#6B7280" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.inputWrapper, isEditing && styles.inputWrapperEditing]}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Send a message..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
onKeyPress={handleKeyPress}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
{/* Send or Stop button */}
|
||||
{isLoading ? (
|
||||
<TouchableOpacity
|
||||
style={styles.stopButton}
|
||||
onPress={handleStop}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Square size={16} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={[styles.sendButton, !text.trim() && styles.sendButtonDisabled]}
|
||||
onPress={handleSend}
|
||||
disabled={!text.trim()}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Send size={18} color={text.trim() ? '#FFFFFF' : '#9CA3AF'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outerContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
},
|
||||
editBanner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: '#EFF6FF',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#DBEAFE',
|
||||
},
|
||||
editBannerText: {
|
||||
fontSize: 13,
|
||||
color: '#2563EB',
|
||||
fontWeight: '500',
|
||||
},
|
||||
editCancelBtn: {
|
||||
padding: 4,
|
||||
},
|
||||
container: {
|
||||
padding: 12,
|
||||
paddingBottom: Platform.OS === 'ios' ? 24 : 12,
|
||||
},
|
||||
inputWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 24,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
minHeight: 44,
|
||||
maxHeight: 120,
|
||||
},
|
||||
inputWrapperEditing: {
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#2563EB',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
color: '#111827',
|
||||
},
|
||||
sendButton: {
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 17,
|
||||
backgroundColor: '#111827',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
sendButtonDisabled: {
|
||||
backgroundColor: '#E5E7EB',
|
||||
},
|
||||
stopButton: {
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 17,
|
||||
backgroundColor: '#EF4444',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Copy, Pencil, RefreshCw, Trash2 } from 'lucide-react-native';
|
||||
import React from 'react';
|
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface MessageActionsProps {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
onEdit?: () => void;
|
||||
onRegenerate?: () => void;
|
||||
onDelete?: () => void;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export function MessageActions({
|
||||
role,
|
||||
content,
|
||||
onEdit,
|
||||
onRegenerate,
|
||||
onDelete,
|
||||
isStreaming,
|
||||
}: MessageActionsProps) {
|
||||
const handleCopy = async () => {
|
||||
await Clipboard.setStringAsync(content);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
onEdit?.();
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
onRegenerate?.();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
onDelete?.();
|
||||
};
|
||||
|
||||
if (isStreaming) return null;
|
||||
|
||||
const isUser = role === 'user';
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isUser ? styles.containerUser : styles.containerAssistant]}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleCopy} activeOpacity={0.6}>
|
||||
<Copy size={14} color="#9CA3AF" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{isUser && onEdit && (
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleEdit} activeOpacity={0.6}>
|
||||
<Pencil size={14} color="#9CA3AF" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{!isUser && onRegenerate && (
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleRegenerate} activeOpacity={0.6}>
|
||||
<RefreshCw size={14} color="#9CA3AF" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleDelete} activeOpacity={0.6}>
|
||||
<Trash2 size={14} color="#9CA3AF" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
marginTop: 4,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
containerUser: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
containerAssistant: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
actionBtn: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
import { ChatMessage } from '@/store/useAppStore';
|
||||
import Markdown from '@ronradtke/react-native-markdown-display';
|
||||
import { Bot, User } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
||||
import { MessageActions } from './MessageActions';
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: ChatMessage;
|
||||
onEdit?: () => void;
|
||||
onRegenerate?: () => void;
|
||||
onDelete?: () => void;
|
||||
isStreaming?: boolean;
|
||||
isLastAssistant?: boolean;
|
||||
}
|
||||
|
||||
export function MessageBubble({
|
||||
message,
|
||||
onEdit,
|
||||
onRegenerate,
|
||||
onDelete,
|
||||
isStreaming,
|
||||
isLastAssistant,
|
||||
}: MessageBubbleProps) {
|
||||
const { width } = useWindowDimensions();
|
||||
const isLargeScreen = width > 768;
|
||||
const isUser = message.role === 'user';
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
|
||||
const showingStreaming = isStreaming && isLastAssistant;
|
||||
const shouldShowActions = !isUser || showActions;
|
||||
|
||||
const renderContent = () => {
|
||||
if (isUser) {
|
||||
return (
|
||||
<Text style={[styles.text, styles.textUser]}>
|
||||
{message.content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (!message.content && showingStreaming) {
|
||||
return null; // TypingIndicator handled externally
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown style={markdownStyles}>
|
||||
{message.content}
|
||||
</Markdown>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
onTouchStart={() => isUser && setShowActions(true)}
|
||||
>
|
||||
<View style={[
|
||||
styles.container,
|
||||
isUser ? styles.containerUser : styles.containerAssistant,
|
||||
]}>
|
||||
{/* Avatar */}
|
||||
{!isUser && (
|
||||
<View style={styles.avatarAssistant}>
|
||||
<Bot size={16} color="#6B7280" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[
|
||||
styles.bubble,
|
||||
isUser ? styles.bubbleUser : styles.bubbleAssistant,
|
||||
{ maxWidth: isLargeScreen ? '70%' : '85%' },
|
||||
]}>
|
||||
{renderContent()}
|
||||
</View>
|
||||
|
||||
{isUser && (
|
||||
<View style={styles.avatarUser}>
|
||||
<User size={16} color="#FFFFFF" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Timestamp + edited badge */}
|
||||
<View style={[styles.metaRow, isUser ? styles.metaRowUser : styles.metaRowAssistant]}>
|
||||
{!isUser && <View style={{ width: 32 }} />}
|
||||
<Text style={styles.timestamp}>
|
||||
{new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
{message.isEdited ? ' · edited' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Action buttons */}
|
||||
{(shouldShowActions || !isUser) && message.content.length > 0 && (
|
||||
<MessageActions
|
||||
role={message.role}
|
||||
content={message.content}
|
||||
onEdit={isUser ? onEdit : undefined}
|
||||
onRegenerate={!isUser && isLastAssistant ? onRegenerate : undefined}
|
||||
onDelete={onDelete}
|
||||
isStreaming={showingStreaming}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: 8,
|
||||
},
|
||||
containerUser: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
containerAssistant: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
avatarAssistant: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
avatarUser: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#374151',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
bubble: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 18,
|
||||
},
|
||||
bubbleUser: {
|
||||
backgroundColor: '#111827',
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
bubbleAssistant: {
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
text: {
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
textUser: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 2,
|
||||
gap: 8,
|
||||
},
|
||||
metaRowUser: {
|
||||
justifyContent: 'flex-end',
|
||||
paddingRight: 52,
|
||||
},
|
||||
metaRowAssistant: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
timestamp: {
|
||||
fontSize: 11,
|
||||
color: '#9CA3AF',
|
||||
},
|
||||
});
|
||||
|
||||
const markdownStyles = StyleSheet.create({
|
||||
body: {
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
color: '#111827',
|
||||
},
|
||||
strong: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
em: {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: '#E5E7EB',
|
||||
color: '#1F2937',
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 4,
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
fence: {
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#E5E7EB',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
marginVertical: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: '#1F2937',
|
||||
color: '#E5E7EB',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
marginVertical: 8,
|
||||
},
|
||||
blockquote: {
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#D1D5DB',
|
||||
paddingLeft: 12,
|
||||
marginVertical: 4,
|
||||
color: '#6B7280',
|
||||
},
|
||||
bullet_list: {
|
||||
marginVertical: 4,
|
||||
},
|
||||
ordered_list: {
|
||||
marginVertical: 4,
|
||||
},
|
||||
list_item: {
|
||||
marginVertical: 2,
|
||||
},
|
||||
heading1: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
marginVertical: 8,
|
||||
color: '#111827',
|
||||
},
|
||||
heading2: {
|
||||
fontSize: 19,
|
||||
fontWeight: '700',
|
||||
marginVertical: 6,
|
||||
color: '#111827',
|
||||
},
|
||||
heading3: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
marginVertical: 4,
|
||||
color: '#111827',
|
||||
},
|
||||
link: {
|
||||
color: '#2563EB',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
hr: {
|
||||
backgroundColor: '#E5E7EB',
|
||||
height: 1,
|
||||
marginVertical: 12,
|
||||
},
|
||||
paragraph: {
|
||||
marginVertical: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import { ConnectionBadge } from '@/components/ui/ConnectionBadge';
|
||||
import { useLMStudio } from '@/hooks/useLMStudio';
|
||||
import { useAppStore } from '@/store/useAppStore';
|
||||
import { Check, ChevronDown, RefreshCw } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Modal,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
export function ModelSelector() {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const activeModelId = useAppStore(state => state.activeModelId);
|
||||
const setActiveModelId = useAppStore(state => state.setActiveModelId);
|
||||
const { availableModels, isConnected, fetchModels } = useLMStudio();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const handleSelect = (modelId: string) => {
|
||||
setActiveModelId(modelId);
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await fetchModels();
|
||||
setIsRefreshing(false);
|
||||
};
|
||||
|
||||
const activeModelName = activeModelId || 'Select Model';
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={() => setModalVisible(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<ConnectionBadge isConnected={isConnected} compact />
|
||||
<Text style={styles.headerText} numberOfLines={1}>
|
||||
{activeModelName}
|
||||
</Text>
|
||||
<ChevronDown size={16} color="#111827" style={styles.iconOffset} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
activeOpacity={1}
|
||||
onPressOut={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.bottomSheet} onStartShouldSetResponder={() => true}>
|
||||
<View style={styles.dragHandle} />
|
||||
|
||||
<View style={styles.sheetHeader}>
|
||||
<Text style={styles.sheetTitle}>Select Model</Text>
|
||||
<TouchableOpacity
|
||||
onPress={handleRefresh}
|
||||
style={styles.refreshButton}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<ActivityIndicator size="small" color="#6B7280" />
|
||||
) : (
|
||||
<RefreshCw size={18} color="#6B7280" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{!isConnected && (
|
||||
<View style={styles.warningBox}>
|
||||
<Text style={styles.warningText}>
|
||||
Not connected to LM Studio. Configure server in Settings.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{availableModels.length === 0 && isConnected && (
|
||||
<View style={styles.emptyBox}>
|
||||
<Text style={styles.emptyText}>
|
||||
No models loaded. Load a model in LM Studio first.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FlatList
|
||||
data={availableModels}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={({ item }) => {
|
||||
const isSelected = item.id === activeModelId;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.itemRow, isSelected && styles.itemRowSelected]}
|
||||
onPress={() => handleSelect(item.id)}
|
||||
>
|
||||
<View style={styles.itemContent}>
|
||||
<Text style={[styles.itemName, isSelected && styles.itemNameSelected]}>
|
||||
{item.id}
|
||||
</Text>
|
||||
<View style={styles.badgeRow}>
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>API</Text>
|
||||
</View>
|
||||
{item.owned_by && (
|
||||
<View style={[styles.badge, styles.badgeSecondary]}>
|
||||
<Text style={styles.badgeText}>{item.owned_by}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{isSelected && <Check size={20} color="#000000" />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#F3F4F6',
|
||||
gap: 8,
|
||||
maxWidth: 280,
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
flexShrink: 1,
|
||||
},
|
||||
iconOffset: {
|
||||
marginTop: 2,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
bottomSheet: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
padding: 24,
|
||||
paddingTop: 12,
|
||||
maxHeight: '80%',
|
||||
...Platform.select({
|
||||
web: {
|
||||
maxWidth: 480,
|
||||
marginHorizontal: 'auto',
|
||||
borderBottomLeftRadius: 24,
|
||||
borderBottomRightRadius: 24,
|
||||
marginBottom: 24,
|
||||
maxHeight: '60%',
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 24,
|
||||
}
|
||||
})
|
||||
},
|
||||
dragHandle: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: '#E5E7EB',
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginBottom: 16,
|
||||
...Platform.select({ web: { display: 'none' } })
|
||||
},
|
||||
sheetHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
},
|
||||
sheetTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
refreshButton: {
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#F3F4F6',
|
||||
},
|
||||
warningBox: {
|
||||
backgroundColor: '#FEF3C7',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
warningText: {
|
||||
fontSize: 13,
|
||||
color: '#92400E',
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptyBox: {
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
},
|
||||
itemRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
itemRowSelected: {
|
||||
borderColor: '#000000',
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
itemContent: {
|
||||
flex: 1,
|
||||
paddingRight: 16,
|
||||
},
|
||||
itemName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
marginBottom: 6,
|
||||
},
|
||||
itemNameSelected: {
|
||||
color: '#000000',
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 6,
|
||||
},
|
||||
badge: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#DBEAFE',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 6,
|
||||
},
|
||||
badgeSecondary: {
|
||||
backgroundColor: '#F3F4F6',
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#4B5563',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
export function TypingIndicator() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.bubble}>
|
||||
<View style={styles.dotsRow}>
|
||||
<View style={[styles.dot, styles.dot1]} />
|
||||
<View style={[styles.dot, styles.dot2]} />
|
||||
<View style={[styles.dot, styles.dot3]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
paddingHorizontal: 16,
|
||||
marginVertical: 4,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
bubble: {
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 20,
|
||||
borderTopLeftRadius: 4,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
dotsRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#9CA3AF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
dot1: { opacity: 1.0 },
|
||||
dot2: { opacity: 0.7 },
|
||||
dot3: { opacity: 0.4 },
|
||||
});
|
||||
Reference in new issue
Block a user