'use client'; import SendSvg from '@/assets/icons/svg/send.svg'; import SunSvg from '@/assets/icons/svg/sun.svg'; import VideoSvg from '@/assets/icons/svg/video.svg'; import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; import { Keyboard, ScrollView, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; import { webSocketManager, WsMessage } from '@/lib/websocket-util'; import { Message } from '@/types/ask'; import { useTranslation } from 'react-i18next'; import { ThemedText } from '../ThemedText'; import { createNewConversation } from './utils'; interface Props { setIsHello: Dispatch>, conversationId: string | null, setUserMessages: Dispatch>; setConversationId: (conversationId: string) => void, selectedImages: string[]; setSelectedImages: Dispatch>; } const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒 export default function SendMessage(props: Props) { const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props; const { t } = useTranslation() // 用户询问 const [inputValue, setInputValue] = useState(''); // 添加一个ref来跟踪键盘状态 const isKeyboardVisible = useRef(false); const chunkQueue = useRef([]); const renderInterval = useRef | null>(null); useEffect(() => { const handleChatStream = (message: WsMessage) => { if (message.type !== 'ChatStream' || !message.chunk) return; chunkQueue.current.push(message.chunk); if (!renderInterval.current) { renderInterval.current = setInterval(() => { if (chunkQueue.current.length > 0) { const textToRender = chunkQueue.current.join(''); chunkQueue.current = []; setUserMessages(prevMessages => { if (prevMessages.length === 0) return prevMessages; const lastMessage = prevMessages[prevMessages.length - 1]; if (lastMessage.role !== 'assistant') return prevMessages; const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + textToRender; const updatedLastMessage = { ...lastMessage, content: updatedContent }; return [...prevMessages.slice(0, -1), updatedLastMessage]; }); } else { if (renderInterval.current) { clearInterval(renderInterval.current); renderInterval.current = null; } } }, RENDER_INTERVAL); } }; const handleChatStreamEnd = (message: WsMessage) => { if (message.type !== 'ChatStreamEnd') return; // Stop the timer and process any remaining chunks if (renderInterval.current) { clearInterval(renderInterval.current); renderInterval.current = null; } const remainingText = chunkQueue.current.join(''); chunkQueue.current = []; setUserMessages(prevMessages => { if (prevMessages.length === 0) return prevMessages; const lastMessage = prevMessages[prevMessages.length - 1]; if (lastMessage.role !== 'assistant') return prevMessages; // Apply remaining chunks from the queue const contentWithQueue = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + remainingText; // Create the final updated message object const updatedLastMessage = { ...lastMessage, // Use the final message from ChatStreamEnd if available, otherwise use the content with queued text content: message.message ? message.message.content : contentWithQueue, timestamp: message.message ? message.message.timestamp : lastMessage.timestamp, }; return [...prevMessages.slice(0, -1), updatedLastMessage]; }); }; const handleChatResponse = (message: WsMessage) => { if (message.type === 'ChatResponse' && message.message) { setUserMessages(prev => { const newMessages = prev.filter(item => item.content !== 'keepSearchIng'); return [...newMessages, { ...(message.message as Message), role: 'assistant', }]; }); } } const typedHandleChatStream = handleChatStream as (message: WsMessage) => void; const typedHandleChatStreamEnd = handleChatStreamEnd as (message: WsMessage) => void; const typedHandleChatResponse = handleChatResponse as (message: WsMessage) => void; webSocketManager.subscribe('ChatStream', typedHandleChatStream); webSocketManager.subscribe('ChatStreamEnd', typedHandleChatStreamEnd); webSocketManager.subscribe('ChatResponse', typedHandleChatResponse); return () => { webSocketManager.unsubscribe('ChatStream', typedHandleChatStream); webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd); webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse); if (renderInterval.current) { clearInterval(renderInterval.current); } }; }, [setUserMessages]); useEffect(() => { // 使用keyboardWillShow而不是keyboardDidShow,这样可以在键盘完全显示前更新UI const showSubscription = Keyboard.addListener('keyboardWillShow', () => { isKeyboardVisible.current = true; if (!conversationId) { // 确保在下一个事件循环中更新状态,避免可能的渲染问题 requestAnimationFrame(() => { setIsHello(false); setUserMessages([ { id: Math.random().toString(36).substring(2, 9), content: t("ask:ask.introduction1"), role: 'assistant', timestamp: new Date().toISOString() } ]) }); } }); const hideSubscription = Keyboard.addListener('keyboardWillHide', () => { isKeyboardVisible.current = false; }); return () => { showSubscription.remove(); hideSubscription.remove(); }; }, [conversationId, setIsHello, setUserMessages, t]); // 发送询问 const handleSubmit = useCallback(async () => { const text = inputValue.trim(); // 用户输入信息之后进行后续操作 if (text) { // 将用户输入信息添加到消息列表中 setUserMessages(pre => ([...pre, { id: Math.random().toString(36).substring(2, 9), content: text, role: 'user', timestamp: new Date().toISOString() }, { id: Math.random().toString(36).substring(2, 9), content: "keepSearchIng", role: 'assistant', timestamp: new Date().toISOString() } ])); let currentSessionId = conversationId; // 如果没有对话ID,先创建一个新对话 if (!currentSessionId) { currentSessionId = await createNewConversation(text); setConversationId(currentSessionId); webSocketManager.send({ type: 'Chat', session_id: currentSessionId, message: text, image_material_ids: selectedImages.length > 0 ? selectedImages : undefined, }); setSelectedImages([]); } // 通过 WebSocket 发送消息 if (currentSessionId) { webSocketManager.send({ type: 'Chat', session_id: currentSessionId, message: text, image_material_ids: selectedImages.length > 0 ? selectedImages : undefined, }); setSelectedImages([]); } else { console.error("无法获取 session_id,消息发送失败。"); // 可以在这里处理错误,例如显示一个提示 setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); } // 将输入框清空 setInputValue(''); // 只有在键盘可见时才关闭键盘 if (isKeyboardVisible.current) { Keyboard.dismiss(); } } }, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]); const handleQuitly = (type: string) => { setIsHello(false) setUserMessages(pre => ([ ...pre, { id: Math.random().toString(36).substring(2, 9), content: type === "search" ? t("ask:ask.introduction2") : t("ask:ask.introduction3"), role: 'assistant', timestamp: new Date().toISOString() } ])) }; return ( handleQuitly('search')}> {t("ask:ask.search")} handleQuitly('video')}> {t("ask:ask.video")} { setInputValue(text); }} onSubmitEditing={handleSubmit} // 调起的键盘类型 returnKeyType="send" /> ); } const styles = StyleSheet.create({ button: { paddingHorizontal: 8, paddingVertical: 4, margin: 5, borderRadius: 25, alignItems: 'center', borderWidth: 2, display: 'flex', flexDirection: 'row', gap: 5 }, container: { justifyContent: 'center', backgroundColor: '#transparent', }, input: { borderColor: '#FF9500', borderWidth: 1, borderRadius: 25, paddingHorizontal: 20, paddingVertical: 13, fontSize: 16, width: '100%', // 确保输入框宽度撑满 paddingRight: 50 }, voiceButton: { padding: 8, borderRadius: 20, backgroundColor: '#FF9500', justifyContent: 'center', alignItems: 'center', position: 'absolute', transform: [{ translateY: -12 }], }, });