510 lines
21 KiB
TypeScript
510 lines
21 KiB
TypeScript
import ChatSvg from "@/assets/icons/svg/chat.svg";
|
|
import FolderSvg from "@/assets/icons/svg/folder.svg";
|
|
import MoreSvg from "@/assets/icons/svg/more.svg";
|
|
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
|
|
import YesSvg from "@/assets/icons/svg/yes.svg";
|
|
import { Message, Video } from "@/types/ask";
|
|
import { MaterialItem } from "@/types/personal-info";
|
|
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
import React from 'react';
|
|
import {
|
|
FlatList,
|
|
Image,
|
|
Modal,
|
|
Pressable,
|
|
StyleProp,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
ViewStyle
|
|
} from 'react-native';
|
|
import { ThemedText } from "../ThemedText";
|
|
import TypewriterText from "./typewriterText";
|
|
import { mergeArrays } from "./utils";
|
|
|
|
interface RenderMessageProps {
|
|
insets: { top: number };
|
|
item: Message;
|
|
sessionId: string;
|
|
setModalVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, data: Video | MaterialItem }>>;
|
|
modalVisible: { visible: boolean, data: Video | MaterialItem };
|
|
setModalDetailsVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
|
modalDetailsVisible: boolean;
|
|
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
|
selectedImages: string[];
|
|
}
|
|
|
|
const renderMessage = ({ insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => {
|
|
const isUser = item.role === 'User';
|
|
const isVideo = (data: Video | MaterialItem): data is Video => {
|
|
return 'video' in data;
|
|
};
|
|
// 创建一个新的 VideoPlayer 组件
|
|
const VideoPlayer = ({
|
|
videoUrl,
|
|
style,
|
|
onPress
|
|
}: {
|
|
videoUrl: string;
|
|
style?: StyleProp<ViewStyle>;
|
|
onPress?: () => void;
|
|
}) => {
|
|
const player = useVideoPlayer(videoUrl, (player) => {
|
|
player.loop = true;
|
|
player.play();
|
|
});
|
|
|
|
return (
|
|
<Pressable
|
|
style={[{
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
}, style]}
|
|
onPress={onPress}
|
|
>
|
|
<VideoView
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: '#000', // 添加背景色
|
|
}}
|
|
player={player}
|
|
allowsFullscreen
|
|
allowsPictureInPicture
|
|
/>
|
|
</Pressable>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View className={`flex-row items-start gap-2 w-full ${isUser ? 'justify-end' : 'justify-start'}`}>
|
|
{!isUser && <ChatSvg width={36} height={36} />}
|
|
<View className="max-w-[90%] mb-[1rem] flex flex-col gap-2">
|
|
<View
|
|
style={[
|
|
styles.messageBubble,
|
|
isUser ? styles.userBubble : styles.aiBubble
|
|
]}
|
|
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble mr-10 rounded-2xl'} border-0 ${!isUser && (item.content.video_material_infos && item.content.video_material_infos.length > 0 || item.content.image_material_infos && item.content.image_material_infos.length > 0) ? '!rounded-t-3xl !rounded-b-2xl' : '!rounded-3xl'}`}
|
|
>
|
|
<View className={`${isUser ? 'bg-bgPrimary' : 'bg-aiBubble'}`}>
|
|
<Text style={isUser ? styles.userText : styles.aiText}>
|
|
{!isUser
|
|
?
|
|
sessionId ? item.content.text : <TypewriterText text={item.content.text} speed={100} loop={item.content.text == "正在寻找,请稍等..."} />
|
|
: item.content.text
|
|
}
|
|
</Text>
|
|
|
|
{(mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.length || 0 > 0) && (
|
|
<View className="relative">
|
|
<View style={styles.imageGridContainer}>
|
|
{mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image, index, array) => (
|
|
<Pressable
|
|
key={`${image.role}-${image.timestamp}`}
|
|
onPress={() => {
|
|
setModalVisible({ visible: true, data: image });
|
|
}}
|
|
style={({ pressed }) => ({
|
|
width: '32%',
|
|
aspectRatio: 1,
|
|
opacity: pressed ? 0.8 : 1
|
|
})}
|
|
>
|
|
<Image
|
|
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: 12,
|
|
}}
|
|
resizeMode="cover"
|
|
/>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
{
|
|
((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3
|
|
&& <TouchableOpacity className="absolute top-1/2 -translate-y-1/2 -right-4 translate-x-1/2 bg-bgPrimary flex flex-row items-center gap-2 p-1 pl-2 rounded-full" onPress={() => {
|
|
setModalDetailsVisible(true);
|
|
}}>
|
|
<ThemedText className="!text-white font-semibold">{((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0))}</ThemedText>
|
|
<View className="bg-white rounded-full p-2">
|
|
<MoreSvg />
|
|
</View>
|
|
</TouchableOpacity>
|
|
}
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
{/* {item.askAgain && item.askAgain.length > 0 && (
|
|
<View className={`mr-10`}>
|
|
{item.askAgain.map((suggestion, index, array) => (
|
|
<TouchableOpacity
|
|
key={suggestion.id}
|
|
className={`bg-yellow-50 rounded-xl px-4 py-2 border border-yellow-200 border-0 mb-2 ${index === array.length - 1 ? 'mb-0 rounded-b-3xl rounded-t-2xl' : 'rounded-2xl'}`}
|
|
>
|
|
<Text className="text-gray-700">{suggestion.text}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)} */}
|
|
<Modal
|
|
animationType="fade"
|
|
transparent={true}
|
|
visible={modalVisible.visible}
|
|
onRequestClose={() => {
|
|
setModalVisible({ visible: false, data: {} as Video | MaterialItem });
|
|
}}>
|
|
<View style={styles.centeredView}>
|
|
<TouchableOpacity
|
|
style={styles.background}
|
|
onPress={() => {
|
|
setModalVisible({ visible: false, data: {} as Video | MaterialItem })
|
|
}}
|
|
/>
|
|
<TouchableOpacity style={styles.modalView} onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}>
|
|
{isVideo(modalVisible.data) ? (
|
|
// 视频播放器
|
|
<TouchableOpacity
|
|
activeOpacity={1}
|
|
style={{
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
maxHeight: "60%",
|
|
}}
|
|
onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}
|
|
>
|
|
<VideoPlayer
|
|
videoUrl={modalVisible.data.video.file_info.url}
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
alignSelf: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}
|
|
/>
|
|
</TouchableOpacity>
|
|
) : (
|
|
// 图片预览
|
|
<TouchableOpacity
|
|
activeOpacity={1}
|
|
onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}
|
|
style={styles.imageContainer}
|
|
>
|
|
<Image
|
|
source={{ uri: modalVisible.data.preview_file_info?.url }}
|
|
style={styles.fullWidthImage}
|
|
resizeMode="contain"
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</Modal>
|
|
<Modal
|
|
animationType="fade"
|
|
visible={modalDetailsVisible}
|
|
transparent={false}
|
|
statusBarTranslucent={true}
|
|
onRequestClose={() => {
|
|
setModalDetailsVisible(false);
|
|
}}
|
|
>
|
|
<View style={[detailsStyles.container, { paddingTop: insets?.top }]}>
|
|
<View style={detailsStyles.header}>
|
|
<TouchableOpacity onPress={() => setModalDetailsVisible(false)}>
|
|
<ReturnArrow />
|
|
</TouchableOpacity>
|
|
<ThemedText style={detailsStyles.headerText}>Select Photo</ThemedText>
|
|
<FolderSvg />
|
|
</View>
|
|
<View style={{ overflow: 'scroll', height: "100%" }}>
|
|
<FlatList
|
|
data={mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])}
|
|
numColumns={3}
|
|
keyExtractor={(item) => item.id}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={detailsStyles.flatListContent}
|
|
initialNumToRender={12}
|
|
maxToRenderPerBatch={12}
|
|
updateCellsBatchingPeriod={50}
|
|
windowSize={10}
|
|
removeClippedSubviews={true}
|
|
renderItem={({ item }) => {
|
|
return (
|
|
<TouchableOpacity
|
|
style={detailsStyles.gridItemContainer}
|
|
key={item.id}
|
|
>
|
|
<View style={detailsStyles.gridItem}>
|
|
<ThemedText style={detailsStyles.imageNumber}>
|
|
{selectedImages?.map((image, index) => {
|
|
if (image === item.id || image === item.video?.id) {
|
|
return index + 1
|
|
}
|
|
})}
|
|
</ThemedText>
|
|
<Image
|
|
source={{ uri: item?.preview_file_info?.url || item.video?.preview_file_info?.url }}
|
|
style={detailsStyles.image}
|
|
onError={(error) => console.log('Image load error:', error.nativeEvent.error)}
|
|
onLoad={() => console.log('Image loaded successfully')}
|
|
/>
|
|
<TouchableOpacity
|
|
style={[detailsStyles.circleMarker, selectedImages.includes(item?.id || item?.video?.id) ? detailsStyles.circleMarkerSelected : ""]}
|
|
onPress={() => {
|
|
setSelectedImages((prev) => {
|
|
if (prev.includes(item?.id || item?.video?.id)) {
|
|
return prev.filter((id) => id !== (item.id || item?.video?.id));
|
|
} else {
|
|
return [...prev, item.id || item.video?.id];
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
{selectedImages.includes(item?.id || item?.video?.id) ? <YesSvg width={16} height={16} /> : ""}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
}}
|
|
/>
|
|
</View>
|
|
<View style={detailsStyles.footer}>
|
|
<TouchableOpacity
|
|
style={detailsStyles.continueButton}
|
|
onPress={async () => {
|
|
// 如果用户没有选择 则为选择全部
|
|
if (selectedImages?.length < 0) {
|
|
setSelectedImages(mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.map((item) => {
|
|
return item.id || item.video?.id
|
|
}))
|
|
}
|
|
setModalDetailsVisible(false)
|
|
}}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text style={detailsStyles.continueButtonText}>
|
|
Continue Asking
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default renderMessage;
|
|
|
|
const styles = StyleSheet.create({
|
|
imageGridContainer: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-between',
|
|
width: '100%',
|
|
marginTop: 8,
|
|
},
|
|
video: {
|
|
width: '100%',
|
|
height: '100%',
|
|
},
|
|
imageContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
maxHeight: '60%',
|
|
},
|
|
fullWidthImage: {
|
|
width: '100%',
|
|
height: "54%",
|
|
marginBottom: 8,
|
|
},
|
|
gridImage: {
|
|
aspectRatio: 1,
|
|
marginBottom: 8,
|
|
},
|
|
background: {
|
|
...StyleSheet.absoluteFillObject,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)', // 添加半透明黑色背景
|
|
},
|
|
centeredView: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
modalView: {
|
|
borderRadius: 20,
|
|
alignItems: 'center',
|
|
height: '100%',
|
|
width: "100%",
|
|
justifyContent: 'center',
|
|
alignSelf: 'center',
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#f5f5f5',
|
|
},
|
|
userAvatar: {
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: 15,
|
|
},
|
|
messageList: {
|
|
padding: 16,
|
|
},
|
|
messageBubble: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
fontWeight: "600"
|
|
},
|
|
userBubble: {
|
|
alignSelf: 'flex-end',
|
|
backgroundColor: '#FFB645',
|
|
marginLeft: '20%',
|
|
},
|
|
aiBubble: {
|
|
alignSelf: 'flex-start',
|
|
backgroundColor: '#fff',
|
|
marginRight: '20%',
|
|
borderWidth: 1,
|
|
borderColor: '#e5e5ea',
|
|
},
|
|
userText: {
|
|
color: '#4C320C',
|
|
fontSize: 16,
|
|
},
|
|
aiText: {
|
|
color: '#000',
|
|
fontSize: 16,
|
|
},
|
|
});
|
|
|
|
const detailsStyles = StyleSheet.create({
|
|
gridItemContainer: {
|
|
flex: 1, // 使用 flex 布局使项目平均分配空间
|
|
maxWidth: '33.33%', // 每行最多4个项目
|
|
aspectRatio: 1, // 保持1:1的宽高比
|
|
},
|
|
flatListContent: {
|
|
paddingBottom: 100, // 为底部按钮留出更多空间
|
|
paddingHorizontal: 8, // 添加水平内边距
|
|
paddingTop: 8,
|
|
},
|
|
headerText: {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
color: "#4C320C"
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
padding: 0,
|
|
margin: 0,
|
|
backgroundColor: '#fff',
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'relative',
|
|
},
|
|
imageNumber: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
color: '#fff',
|
|
position: 'absolute',
|
|
top: 10,
|
|
left: 10,
|
|
zIndex: 10, // 确保数字显示在图片上方
|
|
},
|
|
imageNumberText: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
color: '#fff',
|
|
},
|
|
numberText: {
|
|
position: 'absolute',
|
|
top: 10,
|
|
left: 10,
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 14,
|
|
backgroundColor: 'rgba(0, 122, 255, 0.9)', // 使用半透明蓝色背景
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 3.84,
|
|
elevation: 5,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: 16,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#eee',
|
|
},
|
|
gridItem: {
|
|
flex: 1, // 填充父容器
|
|
overflow: 'hidden',
|
|
backgroundColor: '#f5f5f5',
|
|
borderWidth: 1,
|
|
borderColor: '#eee',
|
|
height: '100%', // 确保高度填满容器
|
|
position: 'relative',
|
|
},
|
|
image: {
|
|
width: '100%',
|
|
height: '100%',
|
|
resizeMode: 'cover',
|
|
},
|
|
circleMarker: {
|
|
position: 'absolute',
|
|
top: 10,
|
|
right: 10,
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 14,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
borderWidth: 3,
|
|
borderColor: '#fff',
|
|
},
|
|
circleMarkerSelected: {
|
|
backgroundColor: '#FFB645',
|
|
},
|
|
markerText: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
color: '#000',
|
|
},
|
|
footer: {
|
|
position: 'absolute',
|
|
bottom: 20,
|
|
left: 0,
|
|
right: 0,
|
|
paddingHorizontal: 16,
|
|
zIndex: 10,
|
|
paddingVertical: 10,
|
|
},
|
|
continueButton: {
|
|
backgroundColor: '#E2793F',
|
|
borderRadius: 32,
|
|
padding: 16,
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
zIndex: 10,
|
|
},
|
|
continueButtonText: {
|
|
color: '#fff',
|
|
fontSize: 18,
|
|
fontWeight: 'bold',
|
|
}
|
|
}); |