From df8dd38c84b98de61f9a27deaa5c0745e226872e Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 3 Aug 2025 21:33:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B5=81=E5=BC=8F=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ask/hello.tsx | 44 ++++++------ components/ask/send.tsx | 145 ++++++++++++++++++++++++++++++++------- lib/websocket-util.ts | 1 + 3 files changed, 144 insertions(+), 46 deletions(-) diff --git a/components/ask/hello.tsx b/components/ask/hello.tsx index b502a00..58f6152 100644 --- a/components/ask/hello.tsx +++ b/components/ask/hello.tsx @@ -4,7 +4,8 @@ import { Message } from "@/types/ask"; import { Dispatch, SetStateAction } from "react"; import { useTranslation } from "react-i18next"; import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; -import { createNewConversation, getConversation } from "./utils"; +import { webSocketManager } from "@/lib/websocket-util"; +import { createNewConversation } from "./utils"; interface AskHelloProps { setUserMessages: Dispatch>; @@ -15,35 +16,34 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell const { t } = useTranslation(); const handleCase = async (text: string) => { - setIsHello(false) + setIsHello(false); setUserMessages([ { - content: { - text: text - }, - role: 'User', + id: Math.random().toString(36).substring(2, 9), + content: text, + role: 'user', timestamp: new Date().toISOString() }, { - content: { - text: "keepSearchIng" - }, - role: 'Assistant', + id: Math.random().toString(36).substring(2, 9), + content: "keepSearchIng", + role: 'assistant', timestamp: new Date().toISOString() } ]); - const data = await createNewConversation(text); - setConversationId(data); - const response = await getConversation({ session_id: data, user_text: text, material_ids: [] }); - setUserMessages((prev: Message[]) => { - const newMessages = [...(prev || [])]; - if (response) { - newMessages.push(response); - } - return newMessages.filter((item: Message) => - item?.content?.text !== 'keepSearchIng' - ); - }); + + const sessionId = await createNewConversation(text); + if (sessionId) { + setConversationId(sessionId); + webSocketManager.send({ + type: 'Chat', + session_id: sessionId, + message: text + }); + } else { + console.error("Failed to create a new conversation."); + setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); + } } return ( diff --git a/components/ask/send.tsx b/components/ask/send.tsx index 352dc46..b70c028 100644 --- a/components/ask/send.tsx +++ b/components/ask/send.tsx @@ -16,6 +16,7 @@ import { Message } from '@/types/ask'; import { useTranslation } from 'react-i18next'; import { ThemedText } from '../ThemedText'; import { createNewConversation } from './utils'; +import { WsMessage } from '@/lib/websocket-util'; import { webSocketManager } from '@/lib/websocket-util'; interface Props { @@ -26,6 +27,8 @@ interface Props { selectedImages: string[]; setSelectedImages: Dispatch>; } +const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒 + export default function SendMessage(props: Props) { const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props; @@ -35,7 +38,106 @@ export default function SendMessage(props: Props) { const [inputValue, setInputValue] = useState(''); // 添加一个ref来跟踪键盘状态 - const isKeyboardVisible = useRef(false); + 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 @@ -47,10 +149,9 @@ export default function SendMessage(props: Props) { setIsHello(false); setUserMessages([ { - content: { - text: t("ask:ask.introduction1") - }, - role: 'Assistant', + id: Math.random().toString(36).substring(2, 9), + content: t("ask:ask.introduction1"), + role: 'assistant', timestamp: new Date().toISOString() } ]) @@ -66,7 +167,8 @@ export default function SendMessage(props: Props) { showSubscription.remove(); hideSubscription.remove(); }; - }, [conversationId]); + }, [conversationId, setIsHello, setUserMessages, t]); + // 发送询问 const handleSubmit = useCallback(async () => { @@ -75,17 +177,15 @@ export default function SendMessage(props: Props) { if (text) { // 将用户输入信息添加到消息列表中 setUserMessages(pre => ([...pre, { - content: { - text: text - }, - role: 'User', + id: Math.random().toString(36).substring(2, 9), + content: text, + role: 'user', timestamp: new Date().toISOString() }, { - content: { - text: "keepSearchIng" - }, - role: 'Assistant', + id: Math.random().toString(36).substring(2, 9), + content: "keepSearchIng", + role: 'assistant', timestamp: new Date().toISOString() } ])); @@ -108,9 +208,7 @@ export default function SendMessage(props: Props) { } else { console.error("无法获取 session_id,消息发送失败。"); // 可以在这里处理错误,例如显示一个提示 - setUserMessages(prev => prev.filter(item => - !(typeof item.content === 'string' && item.content === 'keepSearchIng') - )); + setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); } // 将输入框清空 setInputValue(''); @@ -119,19 +217,18 @@ export default function SendMessage(props: Props) { Keyboard.dismiss(); } } - }, [inputValue, conversationId, selectedImages, createNewConversation]); + }, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]); const handleQuitly = (type: string) => { setIsHello(false) setUserMessages(pre => ([ ...pre, { - content: { - text: type === "search" - ? t("ask:ask.introduction2") - : t("ask:ask.introduction3") - }, - role: 'Assistant', + 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() } ])) diff --git a/lib/websocket-util.ts b/lib/websocket-util.ts index 7fc7775..1b83926 100644 --- a/lib/websocket-util.ts +++ b/lib/websocket-util.ts @@ -87,6 +87,7 @@ class WebSocketManager { this.ws.onmessage = (event) => { try { const message: WsMessage = JSON.parse(event.data); + // console.log('WebSocket received message:', message) // 根据消息类型分发 const eventListeners = this.messageListeners.get(message.type); if (eventListeners) {