diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 9db1790..ad4378d 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -120,6 +120,16 @@ export default function TabLayout() { tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 }} /> + {/* 对话详情页 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> ); } diff --git a/app/(tabs)/ask.tsx b/app/(tabs)/ask.tsx index c0d4d4b..25cedb6 100644 --- a/app/(tabs)/ask.tsx +++ b/app/(tabs)/ask.tsx @@ -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(null); const [userMessages, setUserMessages] = useState([]); - + // 选择图片 + const [selectedImages, setSelectedImages] = useState([]); // 动画值 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("/chat/new", { method: "POST" }); - setConversationId(data); - }, []); - const { sessionId, newSession } = useLocalSearchParams<{ sessionId: string; newSession: string; @@ -147,7 +138,7 @@ export default function AskScreen() { } ]} > - + @@ -158,6 +149,8 @@ export default function AskScreen() { setUserMessages={setUserMessages} setConversationId={setConversationId} conversationId={conversationId} + selectedImages={selectedImages} + setSelectedImages={setSelectedImages} /> diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index db08fea..1adcf62 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -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(null); - const [isHello, setIsHello] = useState(true); - const [conversationId, setConversationId] = useState(null); - const [userMessages, setUserMessages] = useState([]); + 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("/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(`/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 ( + + 加载中... + + ); + } return ( - - {/* 导航栏 */} - - router.push('/memo-list')} - > - - - MemoWake - - + + { + isLoggedIn ? : + + {/* 标题区域 */} + + + {t('auth.welcomeAwaken.awaken', { ns: 'login' })} + {"\n"} + {t('auth.welcomeAwaken.your', { ns: 'login' })} + {"\n"} + {t('auth.welcomeAwaken.pm', { ns: 'login' })} + + + {t('auth.welcomeAwaken.slogan', { ns: 'login' })} + + - - - {/* 欢迎页面 */} - - - + {/* Memo 形象区域 */} + + + - {/* 聊天页面 */} - - - - - - {/* 输入框 */} - - - - + {/* 介绍文本 */} + + {t('auth.welcomeAwaken.gallery', { ns: 'login' })} + {"\n"} + {t('auth.welcomeAwaken.back', { ns: 'login' })} + + {/* */} + {/* 唤醒按钮 */} + { + router.push('/login') + }} + activeOpacity={0.8} + > + + {t('auth.welcomeAwaken.awake', { ns: 'login' })} + + + + } ); -} - -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', - }, -}); \ No newline at end of file +} \ No newline at end of file diff --git a/assets/icons/svg/folder.svg b/assets/icons/svg/folder.svg new file mode 100644 index 0000000..6653a66 --- /dev/null +++ b/assets/icons/svg/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/yes.svg b/assets/icons/svg/yes.svg new file mode 100644 index 0000000..950e450 --- /dev/null +++ b/assets/icons/svg/yes.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/ask/aiChat.tsx b/components/ask/aiChat.tsx index cddff82..452ccc9 100644 --- a/components/ask/aiChat.tsx +++ b/components/ask/aiChat.tsx @@ -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>; modalVisible: { visible: boolean, data: Video | MaterialItem }; + setModalDetailsVisible: React.Dispatch>; + modalDetailsVisible: boolean; + setSelectedImages: React.Dispatch>; + 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 } - {(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) && ( - + {mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image, index, array) => ( { 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 + })} > @@ -116,12 +127,14 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende { ((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3 - && + && { + setModalDetailsVisible(true); + }}> {((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0))} - + } )} @@ -194,6 +207,96 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende + { + setModalDetailsVisible(false); + }} + > + + + setModalDetailsVisible(false)}> + + + Select Photo + + + + item.id} + showsVerticalScrollIndicator={false} + contentContainerStyle={detailsStyles.flatListContent} + initialNumToRender={12} + maxToRenderPerBatch={12} + updateCellsBatchingPeriod={50} + windowSize={10} + removeClippedSubviews={true} + renderItem={({ item }) => { + return ( + + + + {selectedImages?.map((image, index) => { + if (image === item.id || image === item.video?.id) { + return index + 1 + } + })} + + console.log('Image load error:', error.nativeEvent.error)} + onLoad={() => console.log('Image loaded successfully')} + /> + { + 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) ? : ""} + + + + ); + }} + /> + + + { + // 如果用户没有选择 则为选择全部 + 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} + > + + Continue Asking + + + + + ); @@ -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%', @@ -276,4 +386,125 @@ const styles = StyleSheet.create({ 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', + } }); \ No newline at end of file diff --git a/components/ask/chat.tsx b/components/ask/chat.tsx index 8060359..b6f9020 100644 --- a/components/ask/chat.tsx +++ b/components/ask/chat.tsx @@ -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>; + selectedImages: string[]; } -function ChatComponent({ userMessages, sessionId }: ChatProps) { +function ChatComponent({ userMessages, sessionId, setSelectedImages, selectedImages }: ChatProps) { const flatListRef = useRef(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(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 })} /> ); diff --git a/components/ask/send.tsx b/components/ask/send.tsx index b53e037..b2d01fa 100644 --- a/components/ask/send.tsx +++ b/components/ask/send.tsx @@ -17,9 +17,11 @@ interface Props { conversationId: string | null, setUserMessages: Dispatch>; setConversationId: (conversationId: string) => void, + selectedImages: string[]; + setSelectedImages: Dispatch>; } 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(`/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 }); } // 将输入框清空 diff --git a/components/layout/ask.tsx b/components/layout/ask.tsx index 601a3b3..dcf964a 100644 --- a/components/layout/ask.tsx +++ b/components/layout/ask.tsx @@ -12,7 +12,6 @@ const AskNavbar = () => { const { width } = Dimensions.get('window'); // 获取路由 const pathname = usePathname(); - console.log(pathname); return (