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,
View
} from 'react-native';
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AskScreen() {
@ -30,6 +29,11 @@ export default function AskScreen() {
const insets = useSafeAreaInsets();
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 [conversationId, setConversationId] = useState<string | null>(null);
const [userMessages, setUserMessages] = useState<Message[]>([]);
@ -43,29 +47,49 @@ export default function AskScreen() {
newSession: string;
}>();
// 创建一个可复用的滚动函数
// 创建一个安全的滚动函数
const scrollToEnd = useCallback((animated = true) => {
if (chatListRef.current) {
setTimeout(() => chatListRef.current?.scrollToEnd({ animated }), 100);
if (!isMountedRef.current || !chatListRef.current) return;
// 清理之前的定时器
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(() => {
if (!isHello && userMessages.length > 0) {
if (!isHello && userMessages.length > 0 && isMountedRef.current) {
scrollToEnd();
}
}, [userMessages, isHello, scrollToEnd]);
@ -74,8 +98,12 @@ export default function AskScreen() {
const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
(e) => {
setTimeout(() => {
if (!isHello) {
if (keyboardTimeoutRef.current) {
clearTimeout(keyboardTimeoutRef.current);
}
keyboardTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && !isHello) {
scrollToEnd();
}
}, 100);
@ -85,8 +113,12 @@ export default function AskScreen() {
const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
() => {
setTimeout(() => {
if (!isHello) {
if (keyboardTimeoutRef.current) {
clearTimeout(keyboardTimeoutRef.current);
}
keyboardTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current && !isHello) {
scrollToEnd(false);
}
}, 100);
@ -96,27 +128,29 @@ export default function AskScreen() {
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
if (keyboardTimeoutRef.current) {
clearTimeout(keyboardTimeoutRef.current);
}
};
}, [isHello]);
}, [isHello, scrollToEnd]);
useFocusEffect(
useCallback(() => {
let isMounted = true;
isMountedRef.current = true;
// 使用新的WebSocket订阅hook
const { subscribeToWebSocket } = useWebSocketSubscription(setUserMessages, isMounted);
const { subscribeToWebSocket } = useWebSocketSubscription(setUserMessages, isMountedRef.current);
// 订阅WebSocket消息
const unsubscribe = subscribeToWebSocket();
return () => {
// 设置组件卸载标志
isMounted = false;
// 取消订阅
unsubscribe();
// 执行清理
cleanup();
};
}, [t])
}, [t, cleanup])
);
// 创建动画样式
@ -141,39 +175,50 @@ export default function AskScreen() {
}, [isHello]);
useEffect(() => {
if (sessionId) {
if (sessionId && isMountedRef.current) {
setConversationId(sessionId);
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);
setConversationId(null);
}
}, [sessionId, newSession]);
useEffect(() => {
if (!isHello) {
if (!isHello && isMountedRef.current) {
// 不再自动关闭键盘,让用户手动控制
// 这里可以添加其他需要在隐藏hello界面时执行的逻辑
scrollToEnd(false);
}
}, [isHello]);
}, [isHello, scrollToEnd]);
useFocusEffect(
useCallback(() => {
if (!sessionId) {
if (!sessionId && isMountedRef.current) {
setIsHello(true);
setUserMessages([])
setUserMessages([]);
}
}, [sessionId])
);
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]}>
<TouchableOpacity
@ -238,8 +283,7 @@ export default function AskScreen() {
/>
</View>
</KeyboardAvoidingView>
</View >
</GestureDetector >
</View>
);
}

View File

@ -31,39 +31,67 @@ const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒
export default function SendMessage(props: Props) {
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props;
const { t } = useTranslation()
const { t } = useTranslation();
// 用户询问
const [inputValue, setInputValue] = useState('');
// 添加一个ref来跟踪键盘状态
// 添加组件挂载状态跟踪
const isMountedRef = useRef(true);
const isKeyboardVisible = useRef(false);
const chunkQueue = useRef<string[]>([]);
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(() => {
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);
if (!renderInterval.current) {
renderInterval.current = setInterval(() => {
if (!isMountedRef.current) {
if (renderInterval.current) {
clearInterval(renderInterval.current);
renderInterval.current = null;
}
return;
}
if (chunkQueue.current.length > 0) {
const textToRender = chunkQueue.current.join('');
chunkQueue.current = [];
setUserMessages(prevMessages => {
if (prevMessages.length === 0) return prevMessages;
try {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== 'assistant') return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1];
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 {
if (renderInterval.current) {
@ -76,7 +104,7 @@ export default function SendMessage(props: Props) {
};
const handleChatStreamEnd = (message: WsMessage) => {
if (message.type !== 'ChatStreamEnd') return;
if (!isMountedRef.current || message.type !== 'ChatStreamEnd') return;
// Stop the timer and process any remaining chunks
if (renderInterval.current) {
@ -88,37 +116,47 @@ export default function SendMessage(props: Props) {
chunkQueue.current = [];
setUserMessages(prevMessages => {
if (prevMessages.length === 0) return prevMessages;
try {
if (prevMessages.length === 0) return prevMessages;
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage.role !== 'assistant') 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;
// 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,
};
// 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];
return [...prevMessages.slice(0, -1), updatedLastMessage];
} catch (error) {
console.error('处理ChatStreamEnd消息时出错:', error);
return prevMessages;
}
});
};
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',
}];
if (!isMountedRef.current || message.type !== 'ChatResponse') return;
if (message.message) {
setUserMessages(prevMessages => {
try {
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 typedHandleChatStreamEnd = handleChatStreamEnd as (message: WsMessage) => void;
@ -130,14 +168,21 @@ export default function SendMessage(props: Props) {
webSocketManager.subscribe('ChatResponse', typedHandleChatResponse);
return () => {
webSocketManager.unsubscribe('ChatStream', typedHandleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd);
webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse);
if (renderInterval.current) {
clearInterval(renderInterval.current);
}
webSocketManager.unsubscribe('ChatStream', handleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('ChatResponse', handleChatResponse);
// 执行清理
cleanup();
};
}, [setUserMessages]);
}, [setUserMessages, cleanup]);
// 组件卸载时的清理
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
useEffect(() => {
// 使用keyboardWillShow而不是keyboardDidShow这样可以在键盘完全显示前更新UI
@ -172,11 +217,15 @@ export default function SendMessage(props: Props) {
// 发送询问
const handleSubmit = useCallback(async () => {
if (!inputValue.trim() || !isMountedRef.current) return;
const text = inputValue.trim();
// 用户输入信息之后进行后续操作
if (text) {
// 将用户输入信息添加到消息列表中
setUserMessages(pre => ([...pre, {
setIsHello(false);
// 添加用户消息和占位符助手消息
setUserMessages(prev => [
...prev,
{
id: Math.random().toString(36).substring(2, 9),
content: text,
role: 'user',
@ -188,24 +237,18 @@ export default function SendMessage(props: Props) {
role: 'assistant',
timestamp: new Date().toISOString()
}
]));
]);
try {
let currentSessionId = conversationId;
// 如果没有对话ID先创建一个新对话
if (!currentSessionId) {
currentSessionId = await createNewConversation(text);
setConversationId(currentSessionId);
const webSocketManager = getWebSocketManager();
webSocketManager.send({
type: 'Chat',
session_id: currentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
if (currentSessionId && isMountedRef.current) {
setConversationId(currentSessionId);
}
}
// 通过 WebSocket 发送消息
if (currentSessionId) {
if (currentSessionId && isMountedRef.current) {
const webSocketManager = getWebSocketManager();
webSocketManager.send({
type: 'Chat',
@ -217,9 +260,19 @@ export default function SendMessage(props: Props) {
} else {
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'));
}
// 将输入框清空
}
// 将输入框清空
if (isMountedRef.current) {
setInputValue('');
// 只有在键盘可见时才关闭键盘
if (isKeyboardVisible.current) {
@ -228,8 +281,10 @@ export default function SendMessage(props: Props) {
}
}, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]);
const handleQuitly = (type: string) => {
setIsHello(false)
const handleQuitly = useCallback((type: string) => {
if (!isMountedRef.current) return;
setIsHello(false);
setUserMessages(pre => ([
...pre,
{
@ -240,8 +295,8 @@ export default function SendMessage(props: Props) {
role: 'assistant',
timestamp: new Date().toISOString()
}
]))
};
]));
}, [t, setIsHello, setUserMessages]);
return (
<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 { getWebSocketManager, WsMessage, getWebSocketErrorMessage } from '@/lib/websocket-util';
import { Message, Assistant } from '@/types/ask';
@ -9,10 +9,16 @@ export const useWebSocketSubscription = (
isMounted: boolean
) => {
const { t } = useTranslation();
const isMountedRef = useRef(isMounted);
// 更新挂载状态
useEffect(() => {
isMountedRef.current = isMounted;
}, [isMounted]);
const handleChatStream = useCallback((message: WsMessage) => {
// 确保组件仍然挂载
if (!isMounted) return;
if (!isMountedRef.current) return;
if (message.type === 'ChatStream') {
setUserMessages(prevMessages => {
@ -71,7 +77,7 @@ export const useWebSocketSubscription = (
const handleChatStreamEnd = useCallback((message: WsMessage) => {
// 确保组件仍然挂载
if (!isMounted) return;
if (!isMountedRef.current) return;
if (message.type === 'ChatStreamEnd') {
setUserMessages(prevMessages => {
@ -106,7 +112,7 @@ export const useWebSocketSubscription = (
const handleError = useCallback((message: WsMessage) => {
// 确保组件仍然挂载
if (!isMounted) return;
if (!isMountedRef.current) return;
if (message.type === 'Error') {
console.log(`WebSocket Error: ${message.code} - ${message.message}`);
@ -139,7 +145,7 @@ export const useWebSocketSubscription = (
}
});
}
}, [setUserMessages, isMounted, t]);
}, [setUserMessages, t]);
const subscribeToWebSocket = useCallback(() => {
const webSocketManager = getWebSocketManager();
@ -155,8 +161,8 @@ export const useWebSocketSubscription = (
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('Error', handleError);
// 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度
webSocketManager.disconnect();
// 不要在这里断开连接,因为其他组件可能还在使用
// webSocketManager.disconnect();
};
}, [handleChatStream, handleChatStreamEnd, handleError]);