UNPKG

react-native-langflow-chat

Version:

A native React Native component for integrating LangFlow chat with streaming support, citation bubbles, react-native-marked rendering, and customizable UI

843 lines (837 loc) β€’ 36.9 kB
"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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const vector_icons_1 = require("@expo/vector-icons"); const react_1 = __importStar(require("react")); const react_native_1 = require("react-native"); // Import expo-clipboard for cross-platform support const Clipboard = __importStar(require("expo-clipboard")); // Import components const Loading_1 = require("./components/Loading"); const MessageWithCitations_1 = require("./components/MessageWithCitations"); const utils_1 = require("./utils"); const { width: screenWidth, height: screenHeight } = react_native_1.Dimensions.get("window"); const LangFlowChatWidget = ({ flowId, hostUrl, apiKey, windowTitle = "Chat", chatInputs, chatInputField, tweaks, sessionId, additionalHeaders, placeholder = "Type your message...", placeholderSending = "Sending...", inputType = "chat", outputType = "chat", outputComponent, // Localization props with English defaults errorMessage = "Sorry, there was an error processing your message. Please try again.", fallbackMessage = "I received your message but couldn't generate a proper response.", sourceTooltipTitle = "Source", pageText = "Page", ofText = "of", closeButtonAriaLabel = "Close", chatPosition = "bottom-right", height, width, startOpen = false, debugEnabled = false, enableMarkdown = true, fontSize = 12, botMessageStyle, chatWindowStyle, errorMessageStyle, inputContainerStyle, inputStyle, sendButtonStyle, userMessageStyle, citationBubbleColor = "#4a4a4a", // Dark gray default triggerComponent, triggerButtonStyle, onMessage, onError, onLoad, onModalVisibilityChange, }) => { const [isModalVisible, setIsModalVisible] = (0, react_1.useState)(startOpen); const [messages, setMessages] = (0, react_1.useState)([]); const [inputText, setInputText] = (0, react_1.useState)(""); const [isLoading, setIsLoading] = (0, react_1.useState)(false); const [showScrollToBottom, setShowScrollToBottom] = (0, react_1.useState)(false); const [abortController, setAbortController] = (0, react_1.useState)(null); const scrollViewRef = (0, react_1.useRef)(null); const buttonOpacity = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current; const buttonScale = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current; const currentSessionId = (0, react_1.useRef)(sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`); const scrollTimeoutRef = (0, react_1.useRef)(null); // Create debug loggers const { debugLog, debugError } = (0, utils_1.createDebugLogger)(debugEnabled); (0, react_1.useEffect)(() => { if (onLoad) { onLoad(); } }, [onLoad]); // Notifica i cambiamenti di visibilitΓ  della modale (0, react_1.useEffect)(() => { if (onModalVisibilityChange) { onModalVisibilityChange(isModalVisible); } }, [isModalVisible, onModalVisibilityChange]); // Anima il pulsante quando cambia lo stato di loading (0, react_1.useEffect)(() => { debugLog("🎬 isLoading changed:", isLoading); animateButtonIcon(isLoading); // Se Γ¨ in loading, aggiungi un pulse continuo if (isLoading) { const pulseAnimation = react_native_1.Animated.loop(react_native_1.Animated.sequence([ react_native_1.Animated.timing(buttonScale, { toValue: 1.1, duration: 600, useNativeDriver: true, }), react_native_1.Animated.timing(buttonScale, { toValue: 1, duration: 600, useNativeDriver: true, }), ])); // Inizia il pulse dopo l'animazione iniziale setTimeout(() => { if (isLoading) { // Controlla se Γ¨ ancora in loading pulseAnimation.start(); } }, 500); return () => { pulseAnimation.stop(); }; } // Return esplicito quando non Γ¨ in loading return undefined; }, [isLoading]); const handleCloseModal = () => { setIsModalVisible(false); // Pulisce lo storico dei messaggi quando si chiude la chat setMessages([]); // Reset del testo di input se presente setInputText(""); // Reset dello stato di loading setIsLoading(false); // Reset del pulsante scroll to bottom setShowScrollToBottom(false); // Pulisce il timeout dello scroll if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); scrollTimeoutRef.current = null; } // Annulla eventuali richieste in corso if (abortController) { abortController.abort(); setAbortController(null); } // Genera un nuovo sessionId per la prossima conversazione currentSessionId.current = sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }; const handleStopGeneration = () => { if (abortController) { abortController.abort(); setAbortController(null); setIsLoading(false); // L'animazione Γ¨ ora gestita dal useEffect debugLog("πŸ›‘ Generation stopped by user"); } }; const sendMessageToLangFlow = async (message, onChunk, controller) => { try { const url = `${hostUrl.replace(/\/$/, "")}/api/v1/run/${flowId}?stream=true`; const headers = { "Content-Type": "application/json", ...(apiKey && { "x-api-key": apiKey }), ...additionalHeaders, }; const body = { input_value: message, output_type: outputType, input_type: inputType, ...(outputComponent && { output_component: outputComponent }), ...(tweaks && { tweaks }), ...(chatInputs && { chat_inputs: chatInputs }), ...(chatInputField && { chat_input_field: chatInputField }), session_id: currentSessionId.current, }; // Debug logging debugLog("πŸš€ LangFlow Streaming Request:", { url, method: "POST", headers, body: JSON.stringify(body, null, 2), }); // Use XMLHttpRequest for true real-time streaming return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); let fullResponse = ""; let lastProcessedLength = 0; // Set up abort handling if (controller) { controller.signal.addEventListener("abort", () => { debugLog("πŸ›‘ Aborting XMLHttpRequest..."); xhr.abort(); reject(new Error("AbortError")); }); } xhr.open("POST", url, true); // Set headers Object.entries(headers).forEach(([key, value]) => { xhr.setRequestHeader(key, value); }); // Process chunks as they arrive xhr.onprogress = (event) => { // Get only the new data const newData = xhr.responseText.substring(lastProcessedLength); lastProcessedLength = xhr.responseText.length; if (newData) { debugLog("πŸ“¦ New streaming chunk received:", newData); // Process each line (each line is a complete JSON event) const lines = newData.split("\n"); for (const line of lines) { if (!line.trim()) continue; try { const data = JSON.parse(line.trim()); debugLog("πŸ“¨ Parsed event:", JSON.stringify(data, null, 2)); // Handle "token" events with streaming chunks if (data.event === "token" && data.data && data.data.chunk) { const chunkText = data.data.chunk; debugLog("πŸ’¬ REAL-TIME token chunk:", chunkText); if (chunkText) { fullResponse += chunkText; // Call the chunk callback to update UI in REAL-TIME if (onChunk) { debugLog(`πŸ”₯ REAL-TIME UI update with text (${fullResponse.length} chars)`); onChunk(fullResponse); // Trigger more frequent scrolls during streaming if (scrollViewRef.current) { const isAtBottom = isScrolledToBottom(); if (isAtBottom) { scrollToBottom(true); } } } } } // Handle "end" event else if (data.event === "end" && data.data && data.data.result) { debugLog("🏁 Stream ended with final result"); // Final message handling if needed const result = data.data.result; // Extract final message from the end event if (result.outputs && result.outputs.length > 0) { const output = result.outputs[0]; if (output.outputs && output.outputs.length > 0) { const firstOutput = output.outputs[0]; if (firstOutput.results && firstOutput.results.message) { const finalMessage = firstOutput.results.message.text || firstOutput.results.message; debugLog("πŸ“ Final message from end event:", finalMessage); // Use final message as fallback if streaming didn't capture everything if (finalMessage && finalMessage.length > fullResponse.length) { fullResponse = finalMessage; // Update UI with complete message if needed if (onChunk) { onChunk(fullResponse); } } } } } } // Handle "add_message" events (optional, mainly for logging) else if (data.event === "add_message") { debugLog("πŸ“¨ Message added:", data.data.text); } } catch (parseError) { debugLog("⚠️ Could not parse line as JSON:", line, parseError); } } } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { debugLog("βœ… XMLHttpRequest completed successfully"); resolve(fullResponse || fallbackMessage); } else { debugError("❌ HTTP error:", xhr.status, xhr.statusText); reject(new Error(`HTTP error: ${xhr.status}`)); } }; xhr.onerror = () => { debugError("πŸ’₯ XMLHttpRequest network error"); reject(new Error("Network error")); }; xhr.onabort = () => { debugLog("πŸ›‘ XMLHttpRequest aborted"); reject(new Error("AbortError")); }; // Send the request xhr.send(JSON.stringify(body)); }); } catch (error) { // Handle AbortError separately to avoid logging as error if (error instanceof Error && error.name === "AbortError") { debugLog("πŸ›‘ Stream request was aborted"); throw error; // Re-throw to be handled by caller } debugError("πŸ’₯ LangFlow Streaming API Error:", error); debugError("πŸ’₯ Error Stack:", error instanceof Error ? error.stack : "No stack trace"); throw error; } }; const handleSendMessage = async () => { if (!inputText.trim() || isLoading) return; const userMessage = { id: `user_${Date.now()}`, type: "user", text: inputText.trim(), timestamp: Date.now(), }; setMessages((prev) => [...prev, userMessage]); setInputText(""); setIsLoading(true); // Create AbortController for this request const controller = new AbortController(); setAbortController(controller); // Notify parent component if (onMessage) { onMessage({ type: "user_message", text: userMessage.text, timestamp: userMessage.timestamp, }); } let botMessageId = null; let isAtBottomBeforeResponse = true; let lastScrollTime = 0; // Check if we're at bottom before starting response setTimeout(() => { isAtBottomBeforeResponse = isScrolledToBottom(); }, 100); try { const response = await sendMessageToLangFlow(userMessage.text, // Streaming callback to update the bot message in real-time (chunk) => { const now = Date.now(); const shouldScroll = isAtBottomBeforeResponse && now - lastScrollTime > 100; setMessages((prev) => { // If we don't have a bot message ID yet, create the first bot message if (!botMessageId) { botMessageId = `bot_${Date.now()}`; const botMessage = { id: botMessageId, type: "bot", text: chunk, timestamp: Date.now(), }; // Scroll to bottom on first chunk - immediate setTimeout(() => scrollToBottom(true), 10); lastScrollTime = now; return [...prev, botMessage]; } else { // Update existing bot message const updatedMessages = prev.map((msg) => msg.id === botMessageId ? { ...msg, text: chunk } : msg); // Scroll to bottom on each chunk update if user was already at bottom // Limita la frequenza di scroll a max 10 volte al secondo if (shouldScroll) { setTimeout(() => scrollToBottom(!showScrollToBottom), 10); lastScrollTime = now; } return updatedMessages; } }); }, controller); // Final update with complete response (in case streaming missed something) if (botMessageId) { setMessages((prev) => prev.map((msg) => msg.id === botMessageId ? { ...msg, text: response } : msg)); } // Notify parent component with final message if (onMessage) { onMessage({ type: "bot_message", text: response, timestamp: Date.now(), }); } } catch (error) { // Check if it was aborted by user if (error instanceof Error && error.name === "AbortError") { debugLog("πŸ›‘ Request was aborted by user"); // Remove the bot message if it was created if (botMessageId) { setMessages((prev) => prev.filter((msg) => msg.id !== botMessageId)); } return; } // Remove the bot message and add error message for real errors if (botMessageId) { setMessages((prev) => prev.filter((msg) => msg.id !== botMessageId)); } const errorMessageObj = { id: `error_${Date.now()}`, type: "error", text: errorMessage, timestamp: Date.now(), }; setMessages((prev) => [...prev, errorMessageObj]); // Log and notify parent component only for real errors debugError("πŸ’₯ LangFlow Streaming API Error:", error); debugError("πŸ’₯ Error Stack:", error instanceof Error ? error.stack : "No stack trace"); if (onError) { onError({ message: error instanceof Error ? error.message : "Unknown error", details: error, }); } } finally { setIsLoading(false); setAbortController(null); } }; const scrollToBottom = (immediate = false) => { var _a; if (immediate) { (_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd({ animated: false }); } else { setTimeout(() => { var _a; (_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd({ animated: true }); }, 50); } }; // Funzione per verificare se siamo giΓ  in fondo alla chat const isScrolledToBottom = () => { var _a, _b, _c; if (!scrollViewRef.current) return true; // Accediamo alle proprietΓ  in modo sicuro const contentOffset = scrollViewRef.current; const contentOffsetY = ((_a = contentOffset === null || contentOffset === void 0 ? void 0 : contentOffset.contentOffset) === null || _a === void 0 ? void 0 : _a.y) || 0; const contentHeight = ((_b = contentOffset === null || contentOffset === void 0 ? void 0 : contentOffset.contentSize) === null || _b === void 0 ? void 0 : _b.height) || 0; const scrollViewHeight = ((_c = contentOffset === null || contentOffset === void 0 ? void 0 : contentOffset.layoutMeasurement) === null || _c === void 0 ? void 0 : _c.height) || 0; // Considera "in fondo" se siamo a meno di 100px dal fondo return contentHeight - (contentOffsetY + scrollViewHeight) <= 100; }; const handleScroll = (event) => { // Estrai i valori dall'evento PRIMA del setTimeout per evitare event pooling issues const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; const scrollPosition = contentOffset.y; const contentHeight = contentSize.height; const containerHeight = layoutMeasurement.height; // Clear previous timeout if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } // Debounce scroll handling scrollTimeoutRef.current = setTimeout(() => { // Calcola se siamo vicini al fondo (threshold piΓΉ generoso) const distanceFromBottom = contentHeight - (scrollPosition + containerHeight); const isNearBottom = distanceFromBottom <= 100; // Mostra il pulsante solo se: // 1. Non siamo vicini al fondo // 2. Ci sono messaggi da mostrare // 3. Il contenuto Γ¨ piΓΉ alto del container (c'Γ¨ qualcosa da scrollare) const shouldShow = !isNearBottom && messages.length > 0 && contentHeight > containerHeight + 50; setShowScrollToBottom(shouldShow); }, 100); }; const scrollToBottomPressed = () => { var _a; (_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollToEnd({ animated: true }); setShowScrollToBottom(false); }; const animateButtonIcon = (toLoading) => { debugLog("🎬 Starting button animation, toLoading:", toLoading); // Reset dei valori prima dell'animazione buttonOpacity.setValue(1); buttonScale.setValue(1); react_native_1.Animated.sequence([ // Fase 1: Fade out e scale down drastici react_native_1.Animated.parallel([ react_native_1.Animated.timing(buttonOpacity, { toValue: 0, duration: 200, useNativeDriver: true, }), react_native_1.Animated.timing(buttonScale, { toValue: 0.6, duration: 200, useNativeDriver: true, }), ]), // Fase 2: Fade in e scale up react_native_1.Animated.parallel([ react_native_1.Animated.timing(buttonOpacity, { toValue: 1, duration: 200, useNativeDriver: true, }), react_native_1.Animated.timing(buttonScale, { toValue: 1, duration: 200, useNativeDriver: true, }), ]), ]).start(() => { debugLog("🎬 Button animation completed"); }); }; const handleCopyMessage = async (text) => { try { await Clipboard.setStringAsync(text); debugLog("πŸ“‹ Message copied to clipboard:", text.substring(0, 50) + "..."); } catch (error) { debugLog("❌ Failed to copy to clipboard:", error); } }; (0, react_1.useEffect)(() => { scrollToBottom(); }, [messages]); const renderMessage = (message) => { const isUser = message.type === "user"; const isError = message.type === "error"; const messageContainerStyle = [ styles.messageContainer, isUser ? styles.userMessageContainer : styles.botMessageContainer, ]; const messageStyle = [ styles.messageText, { fontSize: fontSize, lineHeight: Math.round(fontSize * 1.4), // lineHeight proporzionale (1.4x fontSize) }, isUser ? { ...styles.userMessageText, ...userMessageStyle } : isError ? { ...styles.errorMessageText, ...errorMessageStyle } : { ...styles.botMessageText, ...botMessageStyle }, ]; return (<react_native_1.View key={message.id} style={messageContainerStyle}> <react_native_1.View style={[ styles.messageBubble, isUser ? { ...styles.userMessageBubble, ...userMessageStyle } : isError ? { ...styles.errorMessageBubble, ...errorMessageStyle } : { ...styles.botMessageBubble, ...botMessageStyle }, ]}> {/* Copy button */} <react_native_1.TouchableOpacity style={[ styles.copyButton, isUser ? styles.copyButtonUser : styles.copyButtonBot, ]} onPress={() => handleCopyMessage(message.text)} activeOpacity={0.7}> <vector_icons_1.MaterialCommunityIcons name="content-copy" size={16} color={isUser ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.5)"}/> </react_native_1.TouchableOpacity> {/* Message content */} {isUser ? (<react_native_1.View style={styles.userMessageContent}> <react_native_1.Text style={messageStyle}>{message.text}</react_native_1.Text> </react_native_1.View>) : (<MessageWithCitations_1.MessageWithCitations text={message.text} messageStyle={messageStyle} sourceTooltipTitle={sourceTooltipTitle} pageText={pageText} ofText={ofText} citationBubbleColor={citationBubbleColor} enableMarkdown={enableMarkdown && !isError} fontSize={fontSize}/>)} </react_native_1.View> </react_native_1.View>); }; const renderTriggerButton = () => { if (triggerComponent) { return (<react_native_1.TouchableOpacity style={[(0, utils_1.getTriggerButtonPosition)(chatPosition), triggerButtonStyle]} onPress={() => setIsModalVisible(true)} activeOpacity={0.7}> {triggerComponent} </react_native_1.TouchableOpacity>); } return (<react_native_1.TouchableOpacity style={[ styles.triggerButton, (0, utils_1.getTriggerButtonPosition)(chatPosition), triggerButtonStyle, ]} onPress={() => setIsModalVisible(true)} activeOpacity={0.7}> <react_native_1.View style={styles.triggerButtonContent}> <react_native_1.View style={styles.chatIconContainer}> <vector_icons_1.MaterialCommunityIcons name="message-text-outline" size={24} color="#000000"/> </react_native_1.View> </react_native_1.View> </react_native_1.TouchableOpacity>); }; const modalWidth = width || screenWidth * 0.9; const modalHeight = height || screenHeight * 0.9; return (<> {renderTriggerButton()} <react_native_1.Modal visible={isModalVisible} animationType="slide" transparent={true} onRequestClose={handleCloseModal}> <react_native_1.SafeAreaView style={styles.modalOverlay}> <react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === "ios" ? "padding" : "height"} style={styles.keyboardAvoidingView}> <react_native_1.View style={[ styles.chatContainer, { maxWidth: modalWidth, width: modalWidth, maxHeight: modalHeight, height: modalHeight, }, chatWindowStyle, ]}> {/* Header */} <react_native_1.View style={styles.chatHeader}> <react_native_1.Text style={styles.chatTitle}>{windowTitle}</react_native_1.Text> <react_native_1.TouchableOpacity style={styles.closeButton} onPress={handleCloseModal} accessibilityLabel={closeButtonAriaLabel}> <vector_icons_1.MaterialCommunityIcons name="close" size={18} color="#6c757d"/> </react_native_1.TouchableOpacity> </react_native_1.View> {/* Messages */} <react_native_1.ScrollView ref={scrollViewRef} style={styles.messagesContainer} showsVerticalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={16}> {messages.map(renderMessage)} {isLoading && (<Loading_1.LoadingBubble botMessageStyle={botMessageStyle}/>)} <react_native_1.View style={styles.scrollSpacer}/> </react_native_1.ScrollView> {/* Scroll to Bottom Button */} {showScrollToBottom && (<react_native_1.TouchableOpacity style={styles.scrollToBottomButton} onPress={scrollToBottomPressed} activeOpacity={0.7}> <vector_icons_1.MaterialCommunityIcons name="chevron-down" size={24} color="#666"/> </react_native_1.TouchableOpacity>)} {/* Input */} <react_native_1.View style={[styles.inputContainer, inputContainerStyle]}> <react_native_1.TextInput style={[styles.textInput, inputStyle]} value={inputText} onChangeText={setInputText} placeholder={isLoading ? placeholderSending : placeholder} placeholderTextColor="#999" multiline maxLength={1000} editable={!isLoading}/> <react_native_1.TouchableOpacity style={[ styles.sendButton, sendButtonStyle, !inputText.trim() && !isLoading && styles.sendButtonDisabled, isLoading && styles.sendButtonLoading, ]} onPress={isLoading ? handleStopGeneration : handleSendMessage} disabled={!inputText.trim() && !isLoading}> <react_native_1.Animated.View style={{ opacity: buttonOpacity, transform: [{ scale: buttonScale }], }}> {isLoading ? (<vector_icons_1.MaterialCommunityIcons name="stop" size={20} color="white"/>) : (<vector_icons_1.MaterialCommunityIcons name="send" size={20} color="white"/>)} </react_native_1.Animated.View> </react_native_1.TouchableOpacity> </react_native_1.View> </react_native_1.View> </react_native_1.KeyboardAvoidingView> </react_native_1.SafeAreaView> </react_native_1.Modal> </>); }; const styles = react_native_1.StyleSheet.create({ triggerButton: { width: 56, height: 56, borderRadius: 28, backgroundColor: "#ffffff", justifyContent: "center", alignItems: "center", elevation: 6, shadowColor: "#000", shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.15, shadowRadius: 6, borderWidth: 1, borderColor: "rgba(0, 0, 0, 0.08)", }, modalOverlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.4)", justifyContent: "center", alignItems: "center", paddingHorizontal: 16, }, keyboardAvoidingView: { flex: 1, width: "100%", justifyContent: "center", alignItems: "center", }, chatContainer: { backgroundColor: "#ffffff", borderRadius: 24, overflow: "hidden", elevation: 20, shadowColor: "#000", shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 20, width: "100%", maxWidth: "98%", flex: 1, maxHeight: "98%", }, chatHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 24, paddingTop: 20, paddingBottom: 16, backgroundColor: "#ffffff", }, chatTitle: { fontSize: 22, fontWeight: "600", color: "#000000", letterSpacing: -0.3, }, closeButton: { width: 32, height: 32, borderRadius: 16, backgroundColor: "#f8f9fa", justifyContent: "center", alignItems: "center", borderWidth: 1, borderColor: "rgba(0, 0, 0, 0.06)", }, messagesContainer: { flex: 1, paddingHorizontal: 24, paddingVertical: 16, backgroundColor: "#fafbfc", }, messageContainer: { marginVertical: 4, }, userMessageContent: { paddingRight: 20, }, userMessageContainer: { alignItems: "flex-end", }, botMessageContainer: { alignItems: "flex-start", }, messageBubble: { maxWidth: "80%", paddingHorizontal: 16, paddingVertical: 12, borderRadius: 20, marginVertical: 2, }, userMessageBubble: { backgroundColor: "#000000", borderBottomRightRadius: 6, }, botMessageBubble: { backgroundColor: "#ffffff", borderBottomLeftRadius: 6, borderWidth: 1, borderColor: "rgba(0, 0, 0, 0.08)", }, errorMessageBubble: { backgroundColor: "#ffebee", borderColor: "#ffcdd2", borderWidth: 1, }, messageText: { // fontSize e lineHeight sono ora controllati dinamicamente dal prop fontSize fontWeight: "400", }, userMessageText: { color: "white", fontWeight: "500", }, botMessageText: { color: "#2c2c2c", }, errorMessageText: { color: "#d32f2f", fontWeight: "500", }, inputContainer: { flexDirection: "row", alignItems: "flex-end", paddingHorizontal: 24, paddingVertical: 20, backgroundColor: "#ffffff", borderTopWidth: 1, borderTopColor: "rgba(0, 0, 0, 0.06)", }, textInput: { flex: 1, borderWidth: 1, borderColor: "#e9ecef", borderRadius: 24, paddingHorizontal: 20, paddingVertical: 14, fontSize: 16, maxHeight: 120, backgroundColor: "#f8f9fa", color: "#000000", fontWeight: "400", }, sendButton: { marginLeft: 12, width: 48, height: 48, borderRadius: 24, backgroundColor: "#000000", justifyContent: "center", alignItems: "center", }, sendButtonDisabled: { backgroundColor: "#e9ecef", }, sendButtonLoading: { backgroundColor: "#4a4a4a", }, triggerButtonContent: { flexDirection: "row", alignItems: "center", justifyContent: "center", }, chatIconContainer: { justifyContent: "center", alignItems: "center", }, scrollSpacer: { height: 100, }, scrollToBottomButton: { position: "absolute", bottom: 110, left: "50%", marginLeft: -22, width: 44, height: 44, borderRadius: 22, backgroundColor: "#ffffff", justifyContent: "center", alignItems: "center", elevation: 6, shadowColor: "#000", shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.15, shadowRadius: 6, borderWidth: 1, borderColor: "rgba(0, 0, 0, 0.08)", }, copyButton: { position: "absolute", top: 2, right: 2, width: 30, height: 30, borderRadius: 12, backgroundColor: "rgba(0,0,0,0.1)", justifyContent: "center", alignItems: "center", zIndex: 1, }, copyButtonUser: { backgroundColor: "rgba(0,0,0,0.1)", }, copyButtonBot: { backgroundColor: "rgba(255,255,255,0.1)", }, }); exports.default = LangFlowChatWidget;