import React, { useState, useRef, useCallback } from 'react'; import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, Animated, } from 'react-native'; import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../theme'; import { useTheme } from '../theme'; import { chatAPI } from '../services/api'; interface ChatInterfaceProps { chatbotId: string; welcomeMessage?: string; primaryColor?: string; } interface LocalMessage { id: string; role: 'user' | 'assistant'; content: string; pending?: boolean; time?: string; } function getTime() { return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } export function ChatInterface({ chatbotId, welcomeMessage, primaryColor }: ChatInterfaceProps) { const { theme, isDark } = useTheme(); const accentColor = primaryColor ?? COLORS.primary; const flatRef = useRef(null); const [messages, setMessages] = useState(() => welcomeMessage ? [{ id: 'welcome', role: 'assistant', content: welcomeMessage, time: getTime() }] : [], ); const [input, setInput] = useState(''); const [sending, setSending] = useState(false); const [sessionId, setSessionId] = useState(); const scrollToBottom = useCallback(() => { setTimeout(() => flatRef.current?.scrollToEnd({ animated: true }), 80); }, []); const sendMessage = async () => { const text = input.trim(); if (!text || sending) return; const userMsg: LocalMessage = { id: Date.now().toString(), role: 'user', content: text, time: getTime() }; const loadingMsg: LocalMessage = { id: 'loading', role: 'assistant', content: '', pending: true }; setMessages(prev => [...prev, userMsg, loadingMsg]); setInput(''); setSending(true); scrollToBottom(); try { const data = await chatAPI.sendMessage(chatbotId, text, sessionId); if (!sessionId) setSessionId(data.session_id); setMessages(prev => [ ...prev.filter(m => m.id !== 'loading'), { id: (Date.now() + 1).toString(), role: 'assistant', content: data.response, time: getTime() }, ]); } catch { setMessages(prev => [ ...prev.filter(m => m.id !== 'loading'), { id: (Date.now() + 1).toString(), role: 'assistant', content: "Sorry, I couldn't process your message. Please try again.", time: getTime() }, ]); } finally { setSending(false); scrollToBottom(); } }; const renderMessage = ({ item }: { item: LocalMessage }) => { const isUser = item.role === 'user'; return ( ); }; return ( item.id} renderItem={renderMessage} contentContainerStyle={styles.messageList} showsVerticalScrollIndicator={false} onContentSizeChange={scrollToBottom} /> {/* Input bar */} ); } function MessageBubble({ message, isUser, accentColor, theme, isDark, }: { message: LocalMessage; isUser: boolean; accentColor: string; theme: any; isDark: boolean; }) { const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(8)).current; React.useEffect(() => { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 250, useNativeDriver: true }), Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, speed: 20, bounciness: 2 }), ]).start(); }, []); return ( {!isUser && ( AI )} {message.pending ? ( ) : ( {message.content} )} {message.time && !message.pending ? ( {message.time} ) : null} ); } function TypingIndicator() { const dot1 = useRef(new Animated.Value(0.3)).current; const dot2 = useRef(new Animated.Value(0.3)).current; const dot3 = useRef(new Animated.Value(0.3)).current; React.useEffect(() => { const pulse = (dot: Animated.Value, delay: number) => Animated.loop( Animated.sequence([ Animated.delay(delay), Animated.timing(dot, { toValue: 1, duration: 300, useNativeDriver: true }), Animated.timing(dot, { toValue: 0.3, duration: 300, useNativeDriver: true }), Animated.delay(600 - delay), ]), ); const a1 = pulse(dot1, 0); const a2 = pulse(dot2, 200); const a3 = pulse(dot3, 400); a1.start(); a2.start(); a3.start(); return () => { a1.stop(); a2.stop(); a3.stop(); }; }, []); return ( {[dot1, dot2, dot3].map((dot, i) => ( ))} ); } const typingStyles = StyleSheet.create({ row: { flexDirection: 'row', gap: 5, paddingVertical: 4, paddingHorizontal: 2 }, dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: COLORS.primary }, }); const styles = StyleSheet.create({ container: { flex: 1 }, messageList: { paddingHorizontal: SPACING.md, paddingVertical: SPACING.lg, gap: SPACING.md, paddingBottom: SPACING.xl, }, msgRow: { flexDirection: 'row', gap: SPACING.sm, alignItems: 'flex-end', maxWidth: '100%' }, msgRowUser: { flexDirection: 'row-reverse' }, avatar: { width: 32, height: 32, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center', flexShrink: 0, }, avatarText: { color: COLORS.white, fontSize: 10, fontWeight: '800' }, bubbleWrapper: { flex: 1, gap: 4 }, bubble: { maxWidth: '80%', borderRadius: RADIUS.xl, padding: SPACING.md }, bubbleUser: { alignSelf: 'flex-end', borderBottomRightRadius: RADIUS.xs }, bubbleBot: { alignSelf: 'flex-start', borderWidth: 1, borderBottomLeftRadius: RADIUS.xs, }, bubbleText: { fontSize: FONT_SIZE.md, lineHeight: 22 }, timestamp: { fontSize: 10, paddingHorizontal: 2 }, inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: SPACING.md, paddingVertical: SPACING.md, borderTopWidth: 1, gap: SPACING.sm, }, inputWrapper: { flex: 1, borderRadius: RADIUS.xl, borderWidth: 1.5, paddingHorizontal: SPACING.md, paddingVertical: Platform.OS === 'ios' ? SPACING.sm : 0, maxHeight: 120, }, textInput: { fontSize: FONT_SIZE.md, minHeight: 40, paddingTop: Platform.OS === 'android' ? SPACING.sm : 0, }, sendBtn: { width: 46, height: 46, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center', flexShrink: 0, }, sendBtnDisabled: { opacity: 0.45 }, sendIcon: { color: COLORS.white, fontSize: 20, fontWeight: '700', marginTop: -2 }, });