Some checks failed
Dev Deploy / Explore-Gitea-Actions (push) Failing after 1m27s
320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
'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 { webSocketManager, WsMessage } 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('');
|
||
|
||
// 添加一个ref来跟踪键盘状态
|
||
const isKeyboardVisible = useRef(false);
|
||
const chunkQueue = useRef<string[]>([]);
|
||
const renderInterval = useRef<ReturnType<typeof setInterval> | 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
|
||
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 () => {
|
||
const text = inputValue.trim();
|
||
// 用户输入信息之后进行后续操作
|
||
if (text) {
|
||
// 将用户输入信息添加到消息列表中
|
||
setUserMessages(pre => ([...pre, {
|
||
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()
|
||
}
|
||
]));
|
||
let currentSessionId = conversationId;
|
||
// 如果没有对话ID,先创建一个新对话
|
||
if (!currentSessionId) {
|
||
currentSessionId = await createNewConversation(text);
|
||
setConversationId(currentSessionId);
|
||
webSocketManager.send({
|
||
type: 'Chat',
|
||
session_id: currentSessionId,
|
||
message: text,
|
||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
||
});
|
||
setSelectedImages([]);
|
||
}
|
||
|
||
// 通过 WebSocket 发送消息
|
||
if (currentSessionId) {
|
||
webSocketManager.send({
|
||
type: 'Chat',
|
||
session_id: currentSessionId,
|
||
message: text,
|
||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
||
});
|
||
setSelectedImages([]);
|
||
} else {
|
||
console.error("无法获取 session_id,消息发送失败。");
|
||
// 可以在这里处理错误,例如显示一个提示
|
||
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
||
}
|
||
// 将输入框清空
|
||
setInputValue('');
|
||
// 只有在键盘可见时才关闭键盘
|
||
if (isKeyboardVisible.current) {
|
||
Keyboard.dismiss();
|
||
}
|
||
}
|
||
}, [inputValue, conversationId, selectedImages, createNewConversation, setConversationId, setSelectedImages, setUserMessages]);
|
||
|
||
const handleQuitly = (type: string) => {
|
||
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()
|
||
}
|
||
]))
|
||
};
|
||
|
||
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
|
||
onPress={handleSubmit}
|
||
style={{
|
||
position: 'absolute',
|
||
right: 6,
|
||
bottom: 6
|
||
}}
|
||
>
|
||
<SendSvg />
|
||
</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,
|
||
// backgroundColor: '#F8F8F8'
|
||
},
|
||
container: {
|
||
justifyContent: 'center',
|
||
backgroundColor: '#transparent',
|
||
},
|
||
input: {
|
||
// borderColor: '#d9d9d9',
|
||
borderColor: '#AC7E35',
|
||
borderWidth: 1,
|
||
// borderRadius: 18,
|
||
borderRadius: 25,
|
||
paddingHorizontal: 20,
|
||
paddingVertical: 13,
|
||
lineHeight: 20,
|
||
fontSize: 16,
|
||
width: '100%', // 确保输入框宽度撑满
|
||
paddingRight: 50
|
||
},
|
||
voiceButton: {
|
||
padding: 8,
|
||
borderRadius: 20,
|
||
backgroundColor: '#FF9500',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
marginRight: 8, // 添加一点右边距
|
||
},
|
||
}); |