2025-08-07 01:25:32 +08:00

256 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 { useWebSocketStreamHandler } from '@/hooks/useWebSocketStreamHandler';
import { getWebSocketManager } 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<SetStateAction<boolean>>,
conversationId: string | null,
setUserMessages: Dispatch<SetStateAction<Message[]>>;
setConversationId: (conversationId: string) => void,
selectedImages: string[];
setSelectedImages: Dispatch<SetStateAction<string[]>>;
}
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('');
// 添加组件挂载状态跟踪
const isMountedRef = useRef(true);
const isKeyboardVisible = useRef(false);
// 使用新的WebSocket流处理hook启用批量处理模式
const { subscribeToWebSocket, cleanup } = useWebSocketStreamHandler({
setUserMessages,
isMounted: true, // 传递静态值hook内部会使用ref跟踪
enableBatching: true,
renderInterval: RENDER_INTERVAL
});
// 使用WebSocket订阅
useEffect(() => {
const unsubscribe = subscribeToWebSocket();
return () => {
unsubscribe();
};
}, [subscribeToWebSocket]);
// 组件卸载时的清理
useEffect(() => {
return () => {
isMountedRef.current = false;
cleanup();
};
}, [cleanup]);
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 () => {
if (!inputValue.trim() || !isMountedRef.current) return;
const text = inputValue.trim();
setIsHello(false);
// 添加用户消息和占位符助手消息
setUserMessages(prev => [
...prev,
{
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()
}
]);
try {
let currentSessionId = conversationId;
if (!currentSessionId) {
currentSessionId = await createNewConversation(text);
if (currentSessionId && isMountedRef.current) {
setConversationId(currentSessionId);
}
}
if (currentSessionId && isMountedRef.current) {
const webSocketManager = getWebSocketManager();
webSocketManager.send({
type: 'Chat',
session_id: currentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
} 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) {
Keyboard.dismiss();
}
}
}, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]);
const handleQuitly = useCallback((type: string) => {
if (!isMountedRef.current) return;
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()
}
]));
}, [t, setIsHello, setUserMessages]);
return (
<View style={styles.container}>
<View className="relative w-full">
<ScrollView horizontal={true}>
<TouchableOpacity style={[styles.button, { borderColor: '#FFB645' }]} onPress={() => handleQuitly('search')}>
<SunSvg width={18} height={18} />
<ThemedText>{t("ask:ask.search")}</ThemedText>
</TouchableOpacity><TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
<VideoSvg width={18} height={18} />
<ThemedText>{t("ask:ask.video")}</ThemedText>
</TouchableOpacity>
</ScrollView>
<TextInput
style={styles.input}
placeholder="Ask MeMo Anything..."
placeholderTextColor="#999"
value={inputValue}
onChangeText={(text: string) => {
setInputValue(text);
}}
onSubmitEditing={handleSubmit}
// 调起的键盘类型
returnKeyType="send"
/>
<TouchableOpacity
style={[styles.voiceButton, { bottom: -10 }]}
onPress={handleSubmit}
className="absolute right-2"
>
<View>
<SendSvg color={'white'} width={24} height={24} />
</View>
</TouchableOpacity>
</View>
</View>
);
}
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 }],
},
});