feat: 对话详情

This commit is contained in:
jinyaqiu 2025-07-16 19:51:30 +08:00
parent b8d00ef850
commit 125da0e660
9 changed files with 385 additions and 252 deletions

View File

@ -120,6 +120,16 @@ export default function TabLayout() {
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}} }}
/> />
{/* 对话详情页 */}
<Tabs.Screen
name="chat-details"
options={{
title: 'chat-details',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
</Tabs> </Tabs>
); );
} }

View File

@ -6,7 +6,7 @@ import { ThemedText } from "@/components/ThemedText";
import { fetchApi } from "@/lib/server-api-util"; import { fetchApi } from "@/lib/server-api-util";
import { Message } from "@/types/ask"; import { Message } from "@/types/ask";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { import {
Animated, Animated,
KeyboardAvoidingView, KeyboardAvoidingView,
@ -24,21 +24,12 @@ export default function AskScreen() {
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[]>([]);
// 选择图片
const [selectedImages, setSelectedImages] = useState<string[]>([]);
// 动画值 // 动画值
const fadeAnim = useRef(new Animated.Value(1)).current; const fadeAnim = useRef(new Animated.Value(1)).current;
const fadeAnimChat = useRef(new Animated.Value(0)).current; const fadeAnimChat = useRef(new Animated.Value(0)).current;
const createNewConversation = useCallback(async () => {
setUserMessages([{
content: { text: "请输入您的问题,寻找,请稍等..." },
role: 'Assistant',
timestamp: new Date().toISOString()
}]);
const data = await fetchApi<string>("/chat/new", { method: "POST" });
setConversationId(data);
}, []);
const { sessionId, newSession } = useLocalSearchParams<{ const { sessionId, newSession } = useLocalSearchParams<{
sessionId: string; sessionId: string;
newSession: string; newSession: string;
@ -147,7 +138,7 @@ export default function AskScreen() {
} }
]} ]}
> >
<Chat userMessages={userMessages} sessionId={sessionId} /> <Chat userMessages={userMessages} sessionId={sessionId} setSelectedImages={setSelectedImages} selectedImages={selectedImages} />
</Animated.View> </Animated.View>
</View> </View>
@ -158,6 +149,8 @@ export default function AskScreen() {
setUserMessages={setUserMessages} setUserMessages={setUserMessages}
setConversationId={setConversationId} setConversationId={setConversationId}
conversationId={conversationId} conversationId={conversationId}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
/> />
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>

View File

@ -1,229 +1,111 @@
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg"; import IP from '@/assets/icons/svg/ip.svg';
import Chat from "@/components/ask/chat"; import { registerBackgroundUploadTask, triggerManualUpload } from '@/components/file-upload/backgroundUploader';
import AskHello from "@/components/ask/hello"; import * as MediaLibrary from 'expo-media-library';
import SendMessage from "@/components/ask/send"; import { useRouter } from 'expo-router';
import { ThemedText } from "@/components/ThemedText"; import * as SecureStore from 'expo-secure-store';
import { fetchApi } from "@/lib/server-api-util"; import { useEffect, useState } from 'react';
import { Message } from "@/types/ask"; import { useTranslation } from 'react-i18next';
import { router, useLocalSearchParams } from "expo-router"; import { Platform, Text, TouchableOpacity, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Animated,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import MemoList from './memo-list';
export default function AskScreen() { export default function HomeScreen() {
const router = useRouter();
const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const scrollViewRef = useRef<ScrollView>(null); const [isLoading, setIsLoading] = useState(true);
const [isHello, setIsHello] = useState(true); const [isLoggedIn, setIsLoggedIn] = useState(false);
const [conversationId, setConversationId] = useState<string | null>(null);
const [userMessages, setUserMessages] = useState<Message[]>([]);
// 动画值 useEffect(() => {
const fadeAnim = useRef(new Animated.Value(1)).current; const checkAuthStatus = async () => {
const fadeAnimChat = useRef(new Animated.Value(0)).current; try {
let token;
if (Platform.OS === 'web') {
token = localStorage.getItem('token') || '';
} else {
token = await SecureStore.getItemAsync('token') || '';
}
const createNewConversation = useCallback(async () => { const loggedIn = !!token;
setUserMessages([{ setIsLoggedIn(loggedIn);
content: { text: "请输入您的问题,寻找,请稍等..." }, console.log(loggedIn);
role: 'Assistant',
timestamp: new Date().toISOString() if (loggedIn) {
}]); // 已登录,请求必要的权限
const data = await fetchApi<string>("/chat/new", { method: "POST" }); const { status } = await MediaLibrary.requestPermissionsAsync();
setConversationId(data); if (status === 'granted') {
await registerBackgroundUploadTask();
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
await triggerManualUpload(oneDayAgo, now);
}
router.replace('/ask');
}
setIsLoading(false);
} catch (error) {
console.error('检查登录状态出错:', error);
setIsLoading(false);
} finally {
setIsLoading(false);
}
};
checkAuthStatus();
}, []); }, []);
const { sessionId, newSession } = useLocalSearchParams<{ if (isLoading) {
sessionId: string; return (
newSession: string; <View className="flex-1 bg-bgPrimary justify-center items-center">
}>(); <Text className="text-white">...</Text>
</View>
// 处理滚动到底部 );
useEffect(() => {
if (scrollViewRef.current && !isHello) {
scrollViewRef.current.scrollToEnd({ animated: true });
} }
}, [userMessages, isHello]);
// 处理路由参数
useEffect(() => {
if (sessionId) {
setConversationId(sessionId);
setIsHello(false);
fetchApi<Message[]>(`/chats/${sessionId}/message-history`).then((res) => {
setUserMessages(res);
});
}
if (newSession) {
setIsHello(false);
createNewConversation();
}
}, [sessionId, newSession]);
// 动画效果
useEffect(() => {
if (isHello) {
// 显示欢迎页,隐藏聊天页
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(fadeAnimChat, {
toValue: 0,
duration: 300,
useNativeDriver: true,
})
]).start();
} else {
// 显示聊天页,隐藏欢迎页
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(fadeAnimChat, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
})
]).start();
}
}, [isHello, fadeAnim, fadeAnimChat]);
return ( return (
<View style={[styles.container, { paddingTop: insets.top }]}> <View className="flex-1">
{/* 导航栏 */} {
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}> isLoggedIn ? <MemoList /> :
<View className="flex-1 bg-bgPrimary px-[1rem] h-screen overflow-auto py-[2rem] " style={{ paddingTop: insets.top + 48 }}>
{/* 标题区域 */}
<View className="items-start mb-10 w-full px-5">
<Text className="text-white text-3xl font-bold mb-3 text-left">
{t('auth.welcomeAwaken.awaken', { ns: 'login' })}
{"\n"}
{t('auth.welcomeAwaken.your', { ns: 'login' })}
{"\n"}
{t('auth.welcomeAwaken.pm', { ns: 'login' })}
</Text>
<Text className="text-white/85 text-base text-left">
{t('auth.welcomeAwaken.slogan', { ns: 'login' })}
</Text>
</View>
{/* Memo 形象区域 */}
<View className="items-center">
<IP />
</View>
{/* 介绍文本 */}
<Text className="text-white text-base text-center mb-[1rem] leading-6 opacity-90 px-10 -mt-[4rem]">
{t('auth.welcomeAwaken.gallery', { ns: 'login' })}
{"\n"}
{t('auth.welcomeAwaken.back', { ns: 'login' })}
</Text>
{/* <MessagePush /> */}
{/* 唤醒按钮 */}
<TouchableOpacity <TouchableOpacity
style={styles.backButton} className="bg-white rounded-full px-10 py-4 shadow-[0_2px_4px_rgba(0,0,0,0.1)] w-full items-center"
onPress={() => router.push('/memo-list')} onPress={async () => {
router.push('/login')
}}
activeOpacity={0.8}
> >
<ReturnArrow /> <Text className="text-[#4C320C] font-bold text-lg">
{t('auth.welcomeAwaken.awake', { ns: 'login' })}
</Text>
</TouchableOpacity> </TouchableOpacity>
<ThemedText style={styles.title}>MemoWake</ThemedText>
<View style={styles.placeholder} />
</View> </View>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 0}
enabled={!isHello}
>
<View style={styles.contentContainer}>
{/* 欢迎页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnim,
// 使用 pointerEvents 控制交互
pointerEvents: isHello ? 'auto' : 'none',
zIndex: 1
} }
]}
>
<AskHello />
</Animated.View>
{/* 聊天页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnimChat,
// 使用 pointerEvents 控制交互
pointerEvents: isHello ? 'none' : 'auto',
zIndex: 0
}
]}
>
<Chat userMessages={userMessages} sessionId={sessionId} />
</Animated.View>
</View>
{/* 输入框 */}
<View style={styles.inputContainer}>
<SendMessage
setIsHello={setIsHello}
setUserMessages={setUserMessages}
setConversationId={setConversationId}
conversationId={conversationId}
/>
</View>
</KeyboardAvoidingView>
</View> </View>
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
navbar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
backgroundColor: 'white',
// 使用 border 替代阴影
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.1)',
// 如果需要更柔和的边缘,可以添加一个微妙的阴影
elevation: 1, // Android
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 1,
},
hiddenNavbar: {
shadowOpacity: 0,
elevation: 0,
},
backButton: {
padding: 8,
marginRight: 8,
},
title: {
fontSize: 20,
fontWeight: '600',
textAlign: 'center',
flex: 1,
},
placeholder: {
width: 40,
},
keyboardAvoidingView: {
flex: 1,
},
contentContainer: {
flex: 1,
position: 'relative',
},
absoluteView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'white', // 添加背景色
},
inputContainer: {
padding: 16,
paddingBottom: 24,
backgroundColor: 'white',
borderTopWidth: 1,
borderTopColor: '#f0f0f0',
},
});

View File

@ -0,0 +1,3 @@
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3V13.8C1 14.9201 1 15.4798 1.21799 15.9076C1.40973 16.2839 1.71547 16.5905 2.0918 16.7822C2.5192 17 3.07899 17 4.19691 17H15.8031C16.921 17 17.48 17 17.9074 16.7822C18.2837 16.5905 18.5905 16.2841 18.7822 15.9078C19.0002 15.48 19.0002 14.9199 19.0002 13.7998L19.0002 6.19978C19.0002 5.07967 19.0002 4.51962 18.7822 4.0918C18.5905 3.71547 18.2839 3.40973 17.9076 3.21799C17.4798 3 16.9201 3 15.8 3H10M1 3H10M1 3C1 1.89543 1.89543 1 3 1H6.67452C7.1637 1 7.40886 1 7.63904 1.05526C7.84311 1.10425 8.03785 1.18526 8.2168 1.29492C8.41857 1.41857 8.59181 1.59182 8.9375 1.9375L10 3" stroke="#4C320C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 775 B

3
assets/icons/svg/yes.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4.00001L4.33357 7L11 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@ -1,10 +1,14 @@
import ChatSvg from "@/assets/icons/svg/chat.svg"; 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 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 { Message, Video } from "@/types/ask";
import { MaterialItem } from "@/types/personal-info"; import { MaterialItem } from "@/types/personal-info";
import { useVideoPlayer, VideoView } from 'expo-video'; import { useVideoPlayer, VideoView } from 'expo-video';
import React from 'react'; import React from 'react';
import { import {
FlatList,
Image, Image,
Modal, Modal,
Pressable, Pressable,
@ -20,18 +24,22 @@ import TypewriterText from "./typewriterText";
import { mergeArrays } from "./utils"; import { mergeArrays } from "./utils";
interface RenderMessageProps { interface RenderMessageProps {
insets: { top: number };
item: Message; item: Message;
sessionId: string; sessionId: string;
setModalVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, data: Video | MaterialItem }>>; setModalVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, data: Video | MaterialItem }>>;
modalVisible: { 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 = ({ item, sessionId, setModalVisible, modalVisible }: RenderMessageProps) => { const renderMessage = ({ insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => {
const isUser = item.role === 'User'; const isUser = item.role === 'User';
const isVideo = (data: Video | MaterialItem): data is Video => { const isVideo = (data: Video | MaterialItem): data is Video => {
return 'video' in data; return 'video' in data;
}; };
// 创建一个新的 VideoPlayer 组件 // 创建一个新的 VideoPlayer 组件
const VideoPlayer = ({ const VideoPlayer = ({
videoUrl, videoUrl,
@ -90,25 +98,28 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
} }
</Text> </Text>
{(item.content.image_material_infos && item.content.image_material_infos.length > 0 || item.content.video_material_infos && item.content.video_material_infos.length > 0) && ( {(mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.length || 0 > 0) && (
<View className="relative"> <View className="relative">
<View className="mt-2 flex flex-row gap-2 w-full"> <View style={styles.imageGridContainer}>
{mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image, index, array) => ( {mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image, index, array) => (
<Pressable <Pressable
key={`${image.role}-${image.timestamp}`} key={`${image.role}-${image.timestamp}`}
onPress={() => { onPress={() => {
setModalVisible({ visible: true, data: image }); setModalVisible({ visible: true, data: image });
}} }}
style={({ pressed }) => [ style={({ pressed }) => ({
array.length === 1 ? styles.fullWidthImage : styles.gridImage, width: '32%',
array.length === 2 && { width: '49%' }, aspectRatio: 1,
array.length >= 3 && { width: '32%' }, opacity: pressed ? 0.8 : 1
{ opacity: pressed ? 0.8 : 1 } // 添加按下效果 })}
]}
> >
<Image <Image
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }} source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
className="rounded-xl w-full h-full" style={{
width: '100%',
height: '100%',
borderRadius: 12,
}}
resizeMode="cover" resizeMode="cover"
/> />
</Pressable> </Pressable>
@ -116,12 +127,14 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
</View> </View>
{ {
((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3 ((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3
&& <View 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"> && <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> <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"> <View className="bg-white rounded-full p-2">
<MoreSvg /> <MoreSvg />
</View> </View>
</View> </TouchableOpacity>
} }
</View> </View>
)} )}
@ -194,6 +207,96 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Modal> </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>
</View> </View>
); );
@ -202,6 +305,13 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
export default renderMessage; export default renderMessage;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
imageGridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
width: '100%',
marginTop: 8,
},
video: { video: {
width: '100%', width: '100%',
height: '100%', height: '100%',
@ -277,3 +387,124 @@ const styles = StyleSheet.create({
fontSize: 16, 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',
}
});

View File

@ -1,20 +1,23 @@
import { Message, Video } from '@/types/ask'; import { Message, Video } from '@/types/ask';
import { MaterialItem } from '@/types/personal-info'; import { MaterialItem } from '@/types/personal-info';
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import React, { Dispatch, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { import {
FlatList, FlatList,
SafeAreaView SafeAreaView
} from 'react-native'; } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import renderMessage from "./aiChat"; import renderMessage from "./aiChat";
interface ChatProps { interface ChatProps {
userMessages: Message[]; userMessages: Message[];
sessionId: string; sessionId: string;
setSelectedImages: Dispatch<SetStateAction<string[]>>;
selectedImages: string[];
} }
function ChatComponent({ userMessages, sessionId }: ChatProps) { function ChatComponent({ userMessages, sessionId, setSelectedImages, selectedImages }: ChatProps) {
const flatListRef = useRef<FlatList>(null); const flatListRef = useRef<FlatList>(null);
const insets = useSafeAreaInsets();
const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem }); const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem });
// 使用 useCallback 缓存 keyExtractor 函数 // 使用 useCallback 缓存 keyExtractor 函数
@ -23,6 +26,9 @@ function ChatComponent({ userMessages, sessionId }: ChatProps) {
// 使用 useMemo 缓存样式对象 // 使用 useMemo 缓存样式对象
const contentContainerStyle = useMemo(() => ({ padding: 16 }), []); const contentContainerStyle = useMemo(() => ({ padding: 16 }), []);
// 详情弹窗
const [modalDetailsVisible, setModalDetailsVisible] = useState<boolean>(false);
// 自动滚动到底部 // 自动滚动到底部
useEffect(() => { useEffect(() => {
if (userMessages.length > 0) { if (userMessages.length > 0) {
@ -45,7 +51,7 @@ function ChatComponent({ userMessages, sessionId }: ChatProps) {
updateCellsBatchingPeriod={50} updateCellsBatchingPeriod={50}
initialNumToRender={10} initialNumToRender={10}
windowSize={11} windowSize={11}
renderItem={({ item }) => renderMessage({ item, sessionId, modalVisible, setModalVisible })} renderItem={({ item }) => renderMessage({ setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
/> />
</SafeAreaView> </SafeAreaView>
); );

View File

@ -17,9 +17,11 @@ interface Props {
conversationId: string | null, conversationId: string | null,
setUserMessages: Dispatch<SetStateAction<Message[]>>; setUserMessages: Dispatch<SetStateAction<Message[]>>;
setConversationId: (conversationId: string) => void, setConversationId: (conversationId: string) => void,
selectedImages: string[];
setSelectedImages: Dispatch<SetStateAction<string[]>>;
} }
export default function SendMessage(props: Props) { export default function SendMessage(props: Props) {
const { setIsHello, conversationId, setUserMessages, setConversationId } = props; const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props;
// 用户询问 // 用户询问
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
@ -30,20 +32,23 @@ export default function SendMessage(props: Props) {
method: "POST", method: "POST",
}); });
setConversationId(data); setConversationId(data);
await getConversation({ session_id: data, user_text }); await getConversation({ session_id: data, user_text, material_ids: [] });
}, []); }, []);
// 获取对话信息 // 获取对话信息
const getConversation = useCallback(async ({ session_id, user_text }: { session_id: string, user_text: string }) => { const getConversation = useCallback(async ({ session_id, user_text, material_ids }: { session_id: string, user_text: string, material_ids: string[] }) => {
// 获取对话信息必须要有对话id // 获取对话信息必须要有对话id
if (!session_id) return; if (!session_id) return;
const response = await fetchApi<Message>(`/chat`, { const response = await fetchApi<Message>(`/chat`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
session_id, session_id,
user_text user_text,
material_ids
}) })
}); });
setSelectedImages([]);
setUserMessages((prev: Message[]) => [...prev, response]?.filter((item: Message) => item.content.text !== '正在寻找,请稍等...')); setUserMessages((prev: Message[]) => [...prev, response]?.filter((item: Message) => item.content.text !== '正在寻找,请稍等...'));
}, []); }, []);
@ -74,7 +79,8 @@ export default function SendMessage(props: Props) {
} else { } else {
getConversation({ getConversation({
session_id: conversationId, session_id: conversationId,
user_text: text user_text: text,
material_ids: selectedImages
}); });
} }
// 将输入框清空 // 将输入框清空

View File

@ -12,7 +12,6 @@ const AskNavbar = () => {
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
// 获取路由 // 获取路由
const pathname = usePathname(); const pathname = usePathname();
console.log(pathname);
return ( return (
<View className="absolute bottom-0 left-0 right-0 bg-white" style={{ <View className="absolute bottom-0 left-0 right-0 bg-white" style={{