feat: 对话详情
This commit is contained in:
parent
b8d00ef850
commit
125da0e660
@ -120,6 +120,16 @@ export default function TabLayout() {
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 对话详情页 */}
|
||||
<Tabs.Screen
|
||||
name="chat-details"
|
||||
options={{
|
||||
title: 'chat-details',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { ThemedText } from "@/components/ThemedText";
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { Message } from "@/types/ask";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
@ -24,21 +24,12 @@ export default function AskScreen() {
|
||||
const [isHello, setIsHello] = useState(true);
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
const [userMessages, setUserMessages] = useState<Message[]>([]);
|
||||
|
||||
// 选择图片
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([]);
|
||||
// 动画值
|
||||
const fadeAnim = useRef(new Animated.Value(1)).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<{
|
||||
sessionId: 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>
|
||||
</View>
|
||||
|
||||
@ -158,6 +149,8 @@ export default function AskScreen() {
|
||||
setUserMessages={setUserMessages}
|
||||
setConversationId={setConversationId}
|
||||
conversationId={conversationId}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
@ -1,229 +1,111 @@
|
||||
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
|
||||
import Chat from "@/components/ask/chat";
|
||||
import AskHello from "@/components/ask/hello";
|
||||
import SendMessage from "@/components/ask/send";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { Message } from "@/types/ask";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import IP from '@/assets/icons/svg/ip.svg';
|
||||
import { registerBackgroundUploadTask, triggerManualUpload } from '@/components/file-upload/backgroundUploader';
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import { useRouter } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform, Text, TouchableOpacity, View } from 'react-native';
|
||||
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 scrollViewRef = useRef<ScrollView>(null);
|
||||
const [isHello, setIsHello] = useState(true);
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
const [userMessages, setUserMessages] = useState<Message[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
|
||||
// 动画值
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
const fadeAnimChat = useRef(new Animated.Value(0)).current;
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
let token;
|
||||
if (Platform.OS === 'web') {
|
||||
token = localStorage.getItem('token') || '';
|
||||
} else {
|
||||
token = await SecureStore.getItemAsync('token') || '';
|
||||
}
|
||||
|
||||
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 loggedIn = !!token;
|
||||
setIsLoggedIn(loggedIn);
|
||||
console.log(loggedIn);
|
||||
|
||||
if (loggedIn) {
|
||||
// 已登录,请求必要的权限
|
||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||
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<{
|
||||
sessionId: string;
|
||||
newSession: string;
|
||||
}>();
|
||||
|
||||
// 处理滚动到底部
|
||||
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]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
{/* 导航栏 */}
|
||||
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.push('/memo-list')}
|
||||
>
|
||||
<ReturnArrow />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.title}>MemoWake</ThemedText>
|
||||
<View style={styles.placeholder} />
|
||||
</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 className="flex-1 bg-bgPrimary justify-center items-center">
|
||||
<Text className="text-white">加载中...</Text>
|
||||
</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',
|
||||
},
|
||||
});
|
||||
return (
|
||||
<View className="flex-1">
|
||||
{
|
||||
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
|
||||
className="bg-white rounded-full px-10 py-4 shadow-[0_2px_4px_rgba(0,0,0,0.1)] w-full items-center"
|
||||
onPress={async () => {
|
||||
router.push('/login')
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text className="text-[#4C320C] font-bold text-lg">
|
||||
{t('auth.welcomeAwaken.awake', { ns: 'login' })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
3
assets/icons/svg/folder.svg
Normal file
3
assets/icons/svg/folder.svg
Normal 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
3
assets/icons/svg/yes.svg
Normal 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 |
@ -1,10 +1,14 @@
|
||||
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,
|
||||
@ -20,18 +24,22 @@ 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 = ({ item, sessionId, setModalVisible, modalVisible }: RenderMessageProps) => {
|
||||
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,
|
||||
@ -90,25 +98,28 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
}
|
||||
</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="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) => (
|
||||
<Pressable
|
||||
key={`${image.role}-${image.timestamp}`}
|
||||
onPress={() => {
|
||||
setModalVisible({ visible: true, data: image });
|
||||
}}
|
||||
style={({ pressed }) => [
|
||||
array.length === 1 ? styles.fullWidthImage : styles.gridImage,
|
||||
array.length === 2 && { width: '49%' },
|
||||
array.length >= 3 && { width: '32%' },
|
||||
{ opacity: pressed ? 0.8 : 1 } // 添加按下效果
|
||||
]}
|
||||
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 }}
|
||||
className="rounded-xl w-full h-full"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 12,
|
||||
}}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</Pressable>
|
||||
@ -116,12 +127,14 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
</View>
|
||||
{
|
||||
((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>
|
||||
<View className="bg-white rounded-full p-2">
|
||||
<MoreSvg />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
)}
|
||||
@ -194,6 +207,96 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
</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>
|
||||
);
|
||||
@ -202,6 +305,13 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
export default renderMessage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageGridContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@ -277,3 +387,124 @@ const styles = StyleSheet.create({
|
||||
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',
|
||||
}
|
||||
});
|
||||
@ -1,20 +1,23 @@
|
||||
import { Message, Video } from '@/types/ask';
|
||||
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 {
|
||||
FlatList,
|
||||
SafeAreaView
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import renderMessage from "./aiChat";
|
||||
|
||||
interface ChatProps {
|
||||
userMessages: Message[];
|
||||
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 insets = useSafeAreaInsets();
|
||||
const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem });
|
||||
|
||||
// 使用 useCallback 缓存 keyExtractor 函数
|
||||
@ -23,6 +26,9 @@ function ChatComponent({ userMessages, sessionId }: ChatProps) {
|
||||
// 使用 useMemo 缓存样式对象
|
||||
const contentContainerStyle = useMemo(() => ({ padding: 16 }), []);
|
||||
|
||||
// 详情弹窗
|
||||
const [modalDetailsVisible, setModalDetailsVisible] = useState<boolean>(false);
|
||||
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
if (userMessages.length > 0) {
|
||||
@ -45,7 +51,7 @@ function ChatComponent({ userMessages, sessionId }: ChatProps) {
|
||||
updateCellsBatchingPeriod={50}
|
||||
initialNumToRender={10}
|
||||
windowSize={11}
|
||||
renderItem={({ item }) => renderMessage({ item, sessionId, modalVisible, setModalVisible })}
|
||||
renderItem={({ item }) => renderMessage({ setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@ -17,9 +17,11 @@ interface Props {
|
||||
conversationId: string | null,
|
||||
setUserMessages: Dispatch<SetStateAction<Message[]>>;
|
||||
setConversationId: (conversationId: string) => void,
|
||||
selectedImages: string[];
|
||||
setSelectedImages: Dispatch<SetStateAction<string[]>>;
|
||||
}
|
||||
export default function SendMessage(props: Props) {
|
||||
const { setIsHello, conversationId, setUserMessages, setConversationId } = props;
|
||||
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props;
|
||||
|
||||
// 用户询问
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
@ -30,20 +32,23 @@ export default function SendMessage(props: Props) {
|
||||
method: "POST",
|
||||
});
|
||||
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
|
||||
if (!session_id) return;
|
||||
|
||||
const response = await fetchApi<Message>(`/chat`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
session_id,
|
||||
user_text
|
||||
user_text,
|
||||
material_ids
|
||||
})
|
||||
});
|
||||
setSelectedImages([]);
|
||||
setUserMessages((prev: Message[]) => [...prev, response]?.filter((item: Message) => item.content.text !== '正在寻找,请稍等...'));
|
||||
}, []);
|
||||
|
||||
@ -74,7 +79,8 @@ export default function SendMessage(props: Props) {
|
||||
} else {
|
||||
getConversation({
|
||||
session_id: conversationId,
|
||||
user_text: text
|
||||
user_text: text,
|
||||
material_ids: selectedImages
|
||||
});
|
||||
}
|
||||
// 将输入框清空
|
||||
|
||||
@ -12,7 +12,6 @@ const AskNavbar = () => {
|
||||
const { width } = Dimensions.get('window');
|
||||
// 获取路由
|
||||
const pathname = usePathname();
|
||||
console.log(pathname);
|
||||
|
||||
return (
|
||||
<View className="absolute bottom-0 left-0 right-0 bg-white" style={{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user