import ReturnArrow from "@/assets/icons/svg/returnArrow.svg"; import Chat from "@/components/ask/chat"; import AskHello from "@/components/ask/hello"; import SendMessage from "@/components/ask/send"; import { ThemedText } from "@/components/ThemedText"; import { fetchApi } from "@/lib/server-api-util"; import { getWebSocketErrorMessage, webSocketManager, WsMessage } from "@/lib/websocket-util"; import { Assistant, Message } from "@/types/ask"; import { useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from "react-i18next"; import { Animated, FlatList, Keyboard, KeyboardAvoidingView, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function AskScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const chatListRef = useRef(null); const [isHello, setIsHello] = useState(true); const [conversationId, setConversationId] = useState(null); const [userMessages, setUserMessages] = useState([]); const [selectedImages, setSelectedImages] = useState([]); const fadeAnim = useSharedValue(1); const fadeAnimChat = useSharedValue(0); const { t } = useTranslation(); const { sessionId, newSession } = useLocalSearchParams<{ sessionId: string; newSession: string; }>(); // 创建一个可复用的滚动函数 const scrollToEnd = useCallback((animated = true) => { if (chatListRef.current) { setTimeout(() => chatListRef.current?.scrollToEnd({ animated }), 100); } }, []); // 右滑 const gesture = Gesture.Pan() .onEnd((event) => { const { translationX } = event; const threshold = 100; // 滑动阈值 if (translationX > threshold) { // 从左向右滑动,跳转页面 runOnJS(router.replace)("/memo-list"); } }) .minPointers(1) .activeOffsetX([-10, 10]); // 在 X 方向触发的范围 useEffect(() => { if (!isHello && userMessages.length > 0) { scrollToEnd(); } }, [userMessages, isHello, scrollToEnd]); useEffect(() => { const keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', (e) => { setTimeout(() => { if (!isHello) { scrollToEnd(); } }, 100); } ); const keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', () => { setTimeout(() => { if (!isHello) { scrollToEnd(false); } }, 100); } ); return () => { keyboardDidShowListener.remove(); keyboardDidHideListener.remove(); }; }, [isHello]); useFocusEffect( useCallback(() => { // 添加一个标志来检查组件是否已卸载 let isMounted = true; // 确保在组件挂载时才连接 if (isMounted) { webSocketManager.connect(); } const handleChatStream = (message: WsMessage) => { // 确保组件仍然挂载 if (!isMounted) return; if (message.type === 'ChatStream') { setUserMessages(prevMessages => { // 使用 try-catch 包装以防止错误导致应用崩溃 try { const lastMessage = prevMessages[prevMessages.length - 1]; // 检查是否是有效的助手消息 if (!lastMessage || lastMessage.role !== Assistant) { return prevMessages; } // 创建新的消息数组 const newMessages = [...prevMessages]; if (typeof lastMessage.content === 'string') { if (lastMessage.content === 'keepSearchIng') { // 第一次收到流式消息,替换占位符 newMessages[newMessages.length - 1] = { ...lastMessage, content: message.chunk }; } else { // 持续追加流式消息 newMessages[newMessages.length - 1] = { ...lastMessage, content: lastMessage.content + message.chunk }; } } else if (Array.isArray(lastMessage.content)) { // 如果 content 是数组,则更新第一个 text 部分 const textPartIndex = lastMessage.content.findIndex(p => p.type === 'text'); if (textPartIndex !== -1) { const updatedContent = [...lastMessage.content]; updatedContent[textPartIndex] = { ...updatedContent[textPartIndex], text: (updatedContent[textPartIndex].text || '') + message.chunk }; newMessages[newMessages.length - 1] = { ...lastMessage, content: updatedContent }; } } return newMessages; } catch (error) { console.error('处理 ChatStream 消息时出错:', error); // 发生错误时返回原始消息数组 return prevMessages; } }); } }; const handleChatStreamEnd = (message: WsMessage) => { // 确保组件仍然挂载 if (!isMounted) return; if (message.type === 'ChatStreamEnd') { setUserMessages(prevMessages => { // 使用 try-catch 包装以防止错误导致应用崩溃 try { const lastMessage = prevMessages[prevMessages.length - 1]; // 检查是否是有效的助手消息 if (!lastMessage || lastMessage.role !== Assistant) { return prevMessages; } // 使用最终消息替换流式消息,确保 message.message 存在 if (message.message) { const newMessages = [...prevMessages]; newMessages[newMessages.length - 1] = message.message as Message; return newMessages; } else { // 如果最终消息为空,则移除 'keepSearchIng' 占位符 return prevMessages.filter(m => !(typeof m.content === 'string' && m.content === 'keepSearchIng') ); } } catch (error) { console.error('处理 ChatStreamEnd 消息时出错:', error); // 发生错误时返回原始消息数组 return prevMessages; } }); } }; const handleError = (message: WsMessage) => { // 确保组件仍然挂载 if (!isMounted) return; if (message.type === 'Error') { console.log(`WebSocket Error: ${message.code} - ${message.message}`); setUserMessages(prevMessages => { // 使用 try-catch 包装以防止错误导致应用崩溃 try { const lastMessage = prevMessages[prevMessages.length - 1]; // 检查是否是有效的助手消息且包含占位符 if (!lastMessage || lastMessage.role !== Assistant || typeof lastMessage.content !== 'string' || lastMessage.content !== 'keepSearchIng') { return prevMessages; } // 替换占位符为错误消息 const newMessages = [...prevMessages]; newMessages[newMessages.length - 1] = { ...lastMessage, content: getWebSocketErrorMessage(message.code, t) }; return newMessages; } catch (error) { console.error('处理 Error 消息时出错:', error); // 发生错误时返回原始消息数组 return prevMessages; } }); } }; webSocketManager.subscribe('ChatStream', handleChatStream); webSocketManager.subscribe('ChatStreamEnd', handleChatStreamEnd); webSocketManager.subscribe('Error', handleError); return () => { // 设置组件卸载标志 isMounted = false; // 清理订阅 webSocketManager.unsubscribe('ChatStream', handleChatStream); webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd); webSocketManager.unsubscribe('Error', handleError); // 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度 webSocketManager.disconnect(); }; }, [t]) ); // 创建动画样式 const welcomeStyle = useAnimatedStyle(() => { return { opacity: fadeAnim.value, pointerEvents: isHello ? 'auto' : 'none', }; }); const chatStyle = useAnimatedStyle(() => { return { opacity: fadeAnimChat.value, pointerEvents: isHello ? 'none' : 'auto', }; }); // 触发动画 useEffect(() => { fadeAnim.value = withTiming(isHello ? 1 : 0, { duration: 300 }); fadeAnimChat.value = withTiming(isHello ? 0 : 1, { duration: 300 }); }, [isHello]); useEffect(() => { if (sessionId) { setConversationId(sessionId); setIsHello(false); fetchApi(`/chats/${sessionId}/message-history`).then((res) => { setUserMessages(res); }); } if (newSession) { setIsHello(true); setConversationId(null); } }, [sessionId, newSession]); useEffect(() => { if (!isHello) { // 不再自动关闭键盘,让用户手动控制 // 这里可以添加其他需要在隐藏hello界面时执行的逻辑 scrollToEnd(false); } }, [isHello]); useFocusEffect( useCallback(() => { if (!sessionId) { setIsHello(true); setUserMessages([]) } }, [sessionId]) ); return ( {/* 导航栏 */} { try { if (TextInput.State?.currentlyFocusedInput) { const input = TextInput.State.currentlyFocusedInput(); if (input) input.blur(); } } catch (error) { console.log('失去焦点失败:', error); } Keyboard.dismiss(); router.push('/memo-list'); }} > { router.push('/owner') }}>MemoWake {/* 欢迎页面 */} {/* 聊天页面 */} scrollToEnd()} /> {/* 输入框区域 */} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, navbar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 16, paddingHorizontal: 16, borderBottomWidth: 1, borderBottomColor: 'rgba(0,0,0,0.1)', elevation: 1, // Android shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 1, }, hiddenNavbar: { shadowOpacity: 0, elevation: 0, opacity: 0 }, backButton: { padding: 8, marginRight: 8, }, title: { fontSize: 20, fontWeight: '600', textAlign: 'center', flex: 1, }, placeholder: { width: 40, }, contentContainer: { flex: 1, position: 'relative' }, absoluteView: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'white', }, chatContainer: { flex: 1, }, chatContentContainer: { paddingBottom: 20, }, inputContainer: { padding: 16, paddingBottom: 24, backgroundColor: 'white', // borderTopWidth: 1, // borderTopColor: '#f0f0f0', }, });