This commit is contained in:
Junhui Chen 2025-08-07 01:03:38 +08:00
parent 6ac3f69e24
commit 918f4da40e
3 changed files with 215 additions and 110 deletions

View File

@ -21,8 +21,7 @@ import {
TouchableOpacity, TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AskScreen() { export default function AskScreen() {
@ -30,6 +29,11 @@ export default function AskScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const chatListRef = useRef<FlatList>(null); const chatListRef = useRef<FlatList>(null);
const isMountedRef = useRef(true);
const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const keyboardTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const [isHello, setIsHello] = useState(true); const [isHello, setIsHello] = useState(true);
const [conversationId, setConversationId] = useState<string | null>(null); const [conversationId, setConversationId] = useState<string | null>(null);
const [userMessages, setUserMessages] = useState<Message[]>([]); const [userMessages, setUserMessages] = useState<Message[]>([]);
@ -43,29 +47,49 @@ export default function AskScreen() {
newSession: string; newSession: string;
}>(); }>();
// 创建一个可复用的滚动函数 // 创建一个安全的滚动函数
const scrollToEnd = useCallback((animated = true) => { const scrollToEnd = useCallback((animated = true) => {
if (chatListRef.current) { if (!isMountedRef.current || !chatListRef.current) return;
setTimeout(() => chatListRef.current?.scrollToEnd({ animated }), 100);
// 清理之前的定时器
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
scrollTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && chatListRef.current) {
try {
chatListRef.current.scrollToEnd({ animated });
} catch (error) {
console.warn('滚动到底部失败:', error);
}
}
}, 100);
}, []);
// 清理函数
const cleanup = useCallback(() => {
isMountedRef.current = false;
// 清理定时器
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
scrollTimeoutRef.current = null;
}
if (keyboardTimeoutRef.current) {
clearTimeout(keyboardTimeoutRef.current);
keyboardTimeoutRef.current = null;
}
// 取消API请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
} }
}, []); }, []);
// 右滑
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(() => { useEffect(() => {
if (!isHello && userMessages.length > 0) { if (!isHello && userMessages.length > 0 && isMountedRef.current) {
scrollToEnd(); scrollToEnd();
} }
}, [userMessages, isHello, scrollToEnd]); }, [userMessages, isHello, scrollToEnd]);
@ -74,8 +98,12 @@ export default function AskScreen() {
const keyboardDidShowListener = Keyboard.addListener( const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow', 'keyboardDidShow',
(e) => { (e) => {
setTimeout(() => { if (keyboardTimeoutRef.current) {
if (!isHello) { clearTimeout(keyboardTimeoutRef.current);
}
keyboardTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && !isHello) {
scrollToEnd(); scrollToEnd();
} }
}, 100); }, 100);
@ -85,8 +113,12 @@ export default function AskScreen() {
const keyboardDidHideListener = Keyboard.addListener( const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide', 'keyboardDidHide',
() => { () => {
setTimeout(() => { if (keyboardTimeoutRef.current) {
if (!isHello) { clearTimeout(keyboardTimeoutRef.current);
}
keyboardTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && !isHello) {
scrollToEnd(false); scrollToEnd(false);
} }
}, 100); }, 100);
@ -96,27 +128,29 @@ export default function AskScreen() {
return () => { return () => {
keyboardDidShowListener.remove(); keyboardDidShowListener.remove();
keyboardDidHideListener.remove(); keyboardDidHideListener.remove();
if (keyboardTimeoutRef.current) {
clearTimeout(keyboardTimeoutRef.current);
}
}; };
}, [isHello]); }, [isHello, scrollToEnd]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
let isMounted = true; isMountedRef.current = true;
// 使用新的WebSocket订阅hook // 使用新的WebSocket订阅hook
const { subscribeToWebSocket } = useWebSocketSubscription(setUserMessages, isMounted); const { subscribeToWebSocket } = useWebSocketSubscription(setUserMessages, isMountedRef.current);
// 订阅WebSocket消息 // 订阅WebSocket消息
const unsubscribe = subscribeToWebSocket(); const unsubscribe = subscribeToWebSocket();
return () => { return () => {
// 设置组件卸载标志
isMounted = false;
// 取消订阅 // 取消订阅
unsubscribe(); unsubscribe();
// 执行清理
cleanup();
}; };
}, [t]) }, [t, cleanup])
); );
// 创建动画样式 // 创建动画样式
@ -141,39 +175,50 @@ export default function AskScreen() {
}, [isHello]); }, [isHello]);
useEffect(() => { useEffect(() => {
if (sessionId) { if (sessionId && isMountedRef.current) {
setConversationId(sessionId); setConversationId(sessionId);
setIsHello(false); setIsHello(false);
fetchApi<Message[]>(`/chats/${sessionId}/message-history`).then((res) => {
setUserMessages(res); // 创建新的AbortController
abortControllerRef.current = new AbortController();
fetchApi<Message[]>(`/chats/${sessionId}/message-history`, {
signal: abortControllerRef.current.signal
}).then((res) => {
if (isMountedRef.current) {
setUserMessages(res);
}
}).catch((error) => {
if (error.name !== 'AbortError') {
console.error('获取消息历史失败:', error);
}
}); });
} }
if (newSession) { if (newSession && isMountedRef.current) {
setIsHello(true); setIsHello(true);
setConversationId(null); setConversationId(null);
} }
}, [sessionId, newSession]); }, [sessionId, newSession]);
useEffect(() => { useEffect(() => {
if (!isHello) { if (!isHello && isMountedRef.current) {
// 不再自动关闭键盘,让用户手动控制 // 不再自动关闭键盘,让用户手动控制
// 这里可以添加其他需要在隐藏hello界面时执行的逻辑 // 这里可以添加其他需要在隐藏hello界面时执行的逻辑
scrollToEnd(false); scrollToEnd(false);
} }
}, [isHello]); }, [isHello, scrollToEnd]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
if (!sessionId) { if (!sessionId && isMountedRef.current) {
setIsHello(true); setIsHello(true);
setUserMessages([]) setUserMessages([]);
} }
}, [sessionId]) }, [sessionId])
); );
return ( return (
<GestureDetector gesture={gesture}> <View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
{/* 导航栏 */} {/* 导航栏 */}
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}> <View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
<TouchableOpacity <TouchableOpacity
@ -238,8 +283,7 @@ export default function AskScreen() {
/> />
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</View > </View>
</GestureDetector >
); );
} }

View File

@ -31,39 +31,67 @@ const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒
export default function SendMessage(props: Props) { export default function SendMessage(props: Props) {
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props; const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props;
const { t } = useTranslation() const { t } = useTranslation();
// 用户询问 // 用户询问
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
// 添加一个ref来跟踪键盘状态 // 添加组件挂载状态跟踪
const isMountedRef = useRef(true);
const isKeyboardVisible = useRef(false); const isKeyboardVisible = useRef(false);
const chunkQueue = useRef<string[]>([]); const chunkQueue = useRef<string[]>([]);
const renderInterval = useRef<ReturnType<typeof setInterval> | null>(null); const renderInterval = useRef<ReturnType<typeof setInterval> | null>(null);
// 清理函数
const cleanup = useCallback(() => {
isMountedRef.current = false;
// 清理定时器
if (renderInterval.current) {
clearInterval(renderInterval.current);
renderInterval.current = null;
}
// 清理队列
chunkQueue.current = [];
}, []);
useEffect(() => { useEffect(() => {
const handleChatStream = (message: WsMessage) => { const handleChatStream = (message: WsMessage) => {
if (message.type !== 'ChatStream' || !message.chunk) return; if (!isMountedRef.current || message.type !== 'ChatStream' || !message.chunk) return;
chunkQueue.current.push(message.chunk); chunkQueue.current.push(message.chunk);
if (!renderInterval.current) { if (!renderInterval.current) {
renderInterval.current = setInterval(() => { renderInterval.current = setInterval(() => {
if (!isMountedRef.current) {
if (renderInterval.current) {
clearInterval(renderInterval.current);
renderInterval.current = null;
}
return;
}
if (chunkQueue.current.length > 0) { if (chunkQueue.current.length > 0) {
const textToRender = chunkQueue.current.join(''); const textToRender = chunkQueue.current.join('');
chunkQueue.current = []; chunkQueue.current = [];
setUserMessages(prevMessages => { setUserMessages(prevMessages => {
if (prevMessages.length === 0) return prevMessages; try {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1]; const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== 'assistant') return prevMessages; if (lastMessage.role !== 'assistant') return prevMessages;
const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + textToRender; const updatedContent = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + textToRender;
const updatedLastMessage = { ...lastMessage, content: updatedContent }; const updatedLastMessage = { ...lastMessage, content: updatedContent };
return [...prevMessages.slice(0, -1), updatedLastMessage]; return [...prevMessages.slice(0, -1), updatedLastMessage];
} catch (error) {
console.error('处理流式消息时出错:', error);
return prevMessages;
}
}); });
} else { } else {
if (renderInterval.current) { if (renderInterval.current) {
@ -76,7 +104,7 @@ export default function SendMessage(props: Props) {
}; };
const handleChatStreamEnd = (message: WsMessage) => { const handleChatStreamEnd = (message: WsMessage) => {
if (message.type !== 'ChatStreamEnd') return; if (!isMountedRef.current || message.type !== 'ChatStreamEnd') return;
// Stop the timer and process any remaining chunks // Stop the timer and process any remaining chunks
if (renderInterval.current) { if (renderInterval.current) {
@ -88,37 +116,47 @@ export default function SendMessage(props: Props) {
chunkQueue.current = []; chunkQueue.current = [];
setUserMessages(prevMessages => { setUserMessages(prevMessages => {
if (prevMessages.length === 0) return prevMessages; try {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1]; const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== 'assistant') return prevMessages; if (lastMessage.role !== 'assistant') return prevMessages;
// Apply remaining chunks from the queue // Apply remaining chunks from the queue
const contentWithQueue = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + remainingText; const contentWithQueue = (lastMessage.content === 'keepSearchIng' ? '' : lastMessage.content) + remainingText;
// Create the final updated message object // Create the final updated message object
const updatedLastMessage = { const updatedLastMessage = {
...lastMessage, ...lastMessage,
// Use the final message from ChatStreamEnd if available, otherwise use the content with queued text // Use the final message from ChatStreamEnd if available, otherwise use the content with queued text
content: message.message ? message.message.content : contentWithQueue, content: message.message ? message.message.content : contentWithQueue,
timestamp: message.message ? message.message.timestamp : lastMessage.timestamp, timestamp: message.message ? message.message.timestamp : lastMessage.timestamp,
}; };
return [...prevMessages.slice(0, -1), updatedLastMessage]; return [...prevMessages.slice(0, -1), updatedLastMessage];
} catch (error) {
console.error('处理ChatStreamEnd消息时出错:', error);
return prevMessages;
}
}); });
}; };
const handleChatResponse = (message: WsMessage) => { const handleChatResponse = (message: WsMessage) => {
if (message.type === 'ChatResponse' && message.message) { if (!isMountedRef.current || message.type !== 'ChatResponse') return;
setUserMessages(prev => {
const newMessages = prev.filter(item => item.content !== 'keepSearchIng'); if (message.message) {
return [...newMessages, { setUserMessages(prevMessages => {
...(message.message as Message), try {
role: 'assistant', const updatedMessages = [...prevMessages];
}]; updatedMessages[updatedMessages.length - 1] = message.message as Message;
return updatedMessages;
} catch (error) {
console.error('处理聊天响应时出错:', error);
return prevMessages;
}
}); });
} }
} };
const typedHandleChatStream = handleChatStream as (message: WsMessage) => void; const typedHandleChatStream = handleChatStream as (message: WsMessage) => void;
const typedHandleChatStreamEnd = handleChatStreamEnd as (message: WsMessage) => void; const typedHandleChatStreamEnd = handleChatStreamEnd as (message: WsMessage) => void;
@ -130,14 +168,21 @@ export default function SendMessage(props: Props) {
webSocketManager.subscribe('ChatResponse', typedHandleChatResponse); webSocketManager.subscribe('ChatResponse', typedHandleChatResponse);
return () => { return () => {
webSocketManager.unsubscribe('ChatStream', typedHandleChatStream); webSocketManager.unsubscribe('ChatStream', handleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd); webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse); webSocketManager.unsubscribe('ChatResponse', handleChatResponse);
if (renderInterval.current) {
clearInterval(renderInterval.current); // 执行清理
} cleanup();
}; };
}, [setUserMessages]); }, [setUserMessages, cleanup]);
// 组件卸载时的清理
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
useEffect(() => { useEffect(() => {
// 使用keyboardWillShow而不是keyboardDidShow这样可以在键盘完全显示前更新UI // 使用keyboardWillShow而不是keyboardDidShow这样可以在键盘完全显示前更新UI
@ -172,11 +217,15 @@ export default function SendMessage(props: Props) {
// 发送询问 // 发送询问
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {
if (!inputValue.trim() || !isMountedRef.current) return;
const text = inputValue.trim(); const text = inputValue.trim();
// 用户输入信息之后进行后续操作 setIsHello(false);
if (text) {
// 将用户输入信息添加到消息列表中 // 添加用户消息和占位符助手消息
setUserMessages(pre => ([...pre, { setUserMessages(prev => [
...prev,
{
id: Math.random().toString(36).substring(2, 9), id: Math.random().toString(36).substring(2, 9),
content: text, content: text,
role: 'user', role: 'user',
@ -188,24 +237,18 @@ export default function SendMessage(props: Props) {
role: 'assistant', role: 'assistant',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
} }
])); ]);
try {
let currentSessionId = conversationId; let currentSessionId = conversationId;
// 如果没有对话ID先创建一个新对话
if (!currentSessionId) { if (!currentSessionId) {
currentSessionId = await createNewConversation(text); currentSessionId = await createNewConversation(text);
setConversationId(currentSessionId); if (currentSessionId && isMountedRef.current) {
const webSocketManager = getWebSocketManager(); setConversationId(currentSessionId);
webSocketManager.send({ }
type: 'Chat',
session_id: currentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
} }
// 通过 WebSocket 发送消息 if (currentSessionId && isMountedRef.current) {
if (currentSessionId) {
const webSocketManager = getWebSocketManager(); const webSocketManager = getWebSocketManager();
webSocketManager.send({ webSocketManager.send({
type: 'Chat', type: 'Chat',
@ -217,9 +260,19 @@ export default function SendMessage(props: Props) {
} else { } else {
console.error("无法获取 session_id消息发送失败。"); console.error("无法获取 session_id消息发送失败。");
// 可以在这里处理错误,例如显示一个提示 // 可以在这里处理错误,例如显示一个提示
if (isMountedRef.current) {
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
}
}
} catch (error) {
console.error('发送消息时出错:', error);
if (isMountedRef.current) {
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
} }
// 将输入框清空 }
// 将输入框清空
if (isMountedRef.current) {
setInputValue(''); setInputValue('');
// 只有在键盘可见时才关闭键盘 // 只有在键盘可见时才关闭键盘
if (isKeyboardVisible.current) { if (isKeyboardVisible.current) {
@ -228,8 +281,10 @@ export default function SendMessage(props: Props) {
} }
}, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]); }, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]);
const handleQuitly = (type: string) => { const handleQuitly = useCallback((type: string) => {
setIsHello(false) if (!isMountedRef.current) return;
setIsHello(false);
setUserMessages(pre => ([ setUserMessages(pre => ([
...pre, ...pre,
{ {
@ -240,8 +295,8 @@ export default function SendMessage(props: Props) {
role: 'assistant', role: 'assistant',
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
} }
])) ]));
}; }, [t, setIsHello, setUserMessages]);
return ( return (
<View style={styles.container}> <View style={styles.container}>

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getWebSocketManager, WsMessage, getWebSocketErrorMessage } from '@/lib/websocket-util'; import { getWebSocketManager, WsMessage, getWebSocketErrorMessage } from '@/lib/websocket-util';
import { Message, Assistant } from '@/types/ask'; import { Message, Assistant } from '@/types/ask';
@ -9,10 +9,16 @@ export const useWebSocketSubscription = (
isMounted: boolean isMounted: boolean
) => { ) => {
const { t } = useTranslation(); const { t } = useTranslation();
const isMountedRef = useRef(isMounted);
// 更新挂载状态
useEffect(() => {
isMountedRef.current = isMounted;
}, [isMounted]);
const handleChatStream = useCallback((message: WsMessage) => { const handleChatStream = useCallback((message: WsMessage) => {
// 确保组件仍然挂载 // 确保组件仍然挂载
if (!isMounted) return; if (!isMountedRef.current) return;
if (message.type === 'ChatStream') { if (message.type === 'ChatStream') {
setUserMessages(prevMessages => { setUserMessages(prevMessages => {
@ -71,7 +77,7 @@ export const useWebSocketSubscription = (
const handleChatStreamEnd = useCallback((message: WsMessage) => { const handleChatStreamEnd = useCallback((message: WsMessage) => {
// 确保组件仍然挂载 // 确保组件仍然挂载
if (!isMounted) return; if (!isMountedRef.current) return;
if (message.type === 'ChatStreamEnd') { if (message.type === 'ChatStreamEnd') {
setUserMessages(prevMessages => { setUserMessages(prevMessages => {
@ -106,7 +112,7 @@ export const useWebSocketSubscription = (
const handleError = useCallback((message: WsMessage) => { const handleError = useCallback((message: WsMessage) => {
// 确保组件仍然挂载 // 确保组件仍然挂载
if (!isMounted) return; if (!isMountedRef.current) return;
if (message.type === 'Error') { if (message.type === 'Error') {
console.log(`WebSocket Error: ${message.code} - ${message.message}`); console.log(`WebSocket Error: ${message.code} - ${message.message}`);
@ -139,7 +145,7 @@ export const useWebSocketSubscription = (
} }
}); });
} }
}, [setUserMessages, isMounted, t]); }, [setUserMessages, t]);
const subscribeToWebSocket = useCallback(() => { const subscribeToWebSocket = useCallback(() => {
const webSocketManager = getWebSocketManager(); const webSocketManager = getWebSocketManager();
@ -155,8 +161,8 @@ export const useWebSocketSubscription = (
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd); webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('Error', handleError); webSocketManager.unsubscribe('Error', handleError);
// 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度 // 不要在这里断开连接,因为其他组件可能还在使用
webSocketManager.disconnect(); // webSocketManager.disconnect();
}; };
}, [handleChatStream, handleChatStreamEnd, handleError]); }, [handleChatStream, handleChatStreamEnd, handleError]);