From 1c0296807158d70ab151d2ea94c15d82dfb1c332 Mon Sep 17 00:00:00 2001 From: jinyaqiu Date: Wed, 16 Jul 2025 16:10:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20chat=20=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/ask.tsx | 279 +++++++++++++++++++--------------- app/(tabs)/index.tsx | 311 ++++++++++++++++++++++++++------------ components/ask/hello.tsx | 45 +++--- components/ask/send.tsx | 26 +++- components/layout/ask.tsx | 7 +- 5 files changed, 429 insertions(+), 239 deletions(-) diff --git a/app/(tabs)/ask.tsx b/app/(tabs)/ask.tsx index bb5da1d..c0d4d4b 100644 --- a/app/(tabs)/ask.tsx +++ b/app/(tabs)/ask.tsx @@ -6,107 +6,159 @@ import { ThemedText } from "@/components/ThemedText"; import { fetchApi } from "@/lib/server-api-util"; import { Message } from "@/types/ask"; import { router, useLocalSearchParams } from "expo-router"; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, 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"; export default function AskScreen() { const insets = useSafeAreaInsets(); - // 在组件内部添加 ref const scrollViewRef = useRef(null); - // 用于控制是否显示问候页面 const [isHello, setIsHello] = useState(true); - - // 获取对话id const [conversationId, setConversationId] = useState(null); - - // 用户对话信息收集 const [userMessages, setUserMessages] = useState([]); + // 动画值 + const fadeAnim = useRef(new Animated.Value(1)).current; + const fadeAnimChat = useRef(new Animated.Value(0)).current; + const createNewConversation = useCallback(async () => { - // TODO 用户未输入时,显示提示信息 setUserMessages([{ - content: { - text: "请输入您的问题,寻找,请稍等..." - }, + content: { text: "请输入您的问题,寻找,请稍等..." }, role: 'Assistant', timestamp: new Date().toISOString() }]); - const data = await fetchApi("/chat/new", { - method: "POST", - }); + const data = await fetchApi("/chat/new", { method: "POST" }); setConversationId(data); }, []); - // 获取路由参数 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) + setConversationId(sessionId); + setIsHello(false); fetchApi(`/chats/${sessionId}/message-history`).then((res) => { - setUserMessages(res) - }) + setUserMessages(res); + }); } - if (newSession) { - setIsHello(false) - createNewConversation() + // if (newSession) { + // setIsHello(false); + // createNewConversation(); + // } + }, [sessionId]); + + // 动画效果 + 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(); } - }, [sessionId, newSession]) + }, [isHello, fadeAnim, fadeAnimChat]); return ( - - {/* 导航栏 - 保持在顶部 */} - - {/* 点击去memo list 页面 */} + + {/* 导航栏 */} + { - router.replace('/memo-list'); - }} + onPress={() => router.push('/memo-list')} > - MemoWake - + MemoWake + - { - if (scrollViewRef.current && !isHello) { - scrollViewRef.current.scrollToEnd({ animated: true }); - } - }} - > - {/* 内容区域 */} - - {isHello ? : } - - + + {/* 欢迎页面 */} + + + - {/* 功能区 - 放在 KeyboardAvoidingView 内但在 ScrollView 外 */} - - + {/* 聊天页面 */} + + + + + + {/* 输入框 */} + + @@ -114,83 +166,66 @@ export default function AskScreen() { } const styles = StyleSheet.create({ - navbar: { - boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', - backgroundColor: 'white', - zIndex: 10, - }, container: { flex: 1, backgroundColor: 'white', - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - paddingTop: 60 + }, + 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: { - marginLeft: 16, - padding: 12 + padding: 8, + marginRight: 8, }, - content: { - flex: 1, - padding: 20, - alignItems: 'center', - justifyContent: 'center', - }, - description: { - fontSize: 16, - color: '#666', + title: { + fontSize: 20, + fontWeight: '600', textAlign: 'center', - marginBottom: 40, - paddingHorizontal: 20, - lineHeight: 24, + flex: 1, }, - chipsContainer: { - width: "100%", - flexDirection: 'row', - flexWrap: 'nowrap', + placeholder: { + width: 40, + }, + // 更新 keyboardAvoidingView 和 contentContainer 样式 + keyboardAvoidingView: { + flex: 1, + }, + contentContainer: { + flex: 1, justifyContent: 'center', - marginBottom: 40, - display: "flex", - alignItems: "center", - overflow: "scroll", + paddingBottom: 20, }, - chip: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#FFF5E6', - paddingVertical: 10, - paddingHorizontal: 16, - borderRadius: 20, - margin: 5, - }, - chipText: { - marginLeft: 6, - color: '#FF9500', - fontSize: 14, - }, - inputContainer: { - flexDirection: 'row', - padding: 16, - paddingBottom: 30, + absoluteView: { + position: 'absolute', // 保持绝对定位 + top: 0, + left: 0, + right: 0, + bottom: 0, backgroundColor: 'white', }, - input: { - flex: 1, - borderColor: '#FF9500', - borderWidth: 1, - borderRadius: 25, - paddingHorizontal: 20, - paddingVertical: 12, - fontSize: 16, - width: '100%', // 确保输入框宽度撑满 - }, - voiceButton: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: '#FF9500', - justifyContent: 'center', - alignItems: 'center', - marginRight: 8, // 添加一点右边距 + inputContainer: { + padding: 16, + paddingBottom: 24, + backgroundColor: 'white', + borderTopWidth: 1, + borderTopColor: '#f0f0f0', }, }); \ No newline at end of file diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 1d04e65..db08fea 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,108 +1,229 @@ -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 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 { useSafeAreaInsets } from "react-native-safe-area-context"; -import MemoList from './memo-list'; -export default function HomeScreen() { - const router = useRouter(); - const { t } = useTranslation(); +export default function AskScreen() { const insets = useSafeAreaInsets(); - const [isLoading, setIsLoading] = useState(true); - const [isLoggedIn, setIsLoggedIn] = useState(false); + const scrollViewRef = useRef(null); + const [isHello, setIsHello] = useState(true); + const [conversationId, setConversationId] = useState(null); + const [userMessages, setUserMessages] = useState([]); - useEffect(() => { - const checkAuthStatus = async () => { - try { - let token; - if (Platform.OS === 'web') { - token = localStorage.getItem('token') || ''; - } else { - token = await SecureStore.getItemAsync('token') || ''; - } + // 动画值 + const fadeAnim = useRef(new Animated.Value(1)).current; + const fadeAnimChat = useRef(new Animated.Value(0)).current; - const loggedIn = !!token; - setIsLoggedIn(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'); - } - } catch (error) { - console.error('检查登录状态出错:', error); - } finally { - setIsLoading(false); - } - }; - - checkAuthStatus(); + const createNewConversation = useCallback(async () => { + setUserMessages([{ + content: { text: "请输入您的问题,寻找,请稍等..." }, + role: 'Assistant', + timestamp: new Date().toISOString() + }]); + const data = await fetchApi("/chat/new", { method: "POST" }); + setConversationId(data); }, []); - if (isLoading) { - return ( - - 加载中... - - ); - } + 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]); return ( - - { - 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' })} - - + + {/* 导航栏 */} + + router.push('/memo-list')} + > + + + MemoWake + + - {/* 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' })} - - - - } + {/* 聊天页面 */} + + + + + + {/* 输入框 */} + + + + ); -} \ No newline at end of file +} + +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 diff --git a/components/ask/hello.tsx b/components/ask/hello.tsx index 45bc690..e47aea1 100644 --- a/components/ask/hello.tsx +++ b/components/ask/hello.tsx @@ -1,28 +1,39 @@ import IP from "@/assets/icons/svg/ip.svg"; import { ThemedText } from "@/components/ThemedText"; import { useTranslation } from "react-i18next"; -import { View } from 'react-native'; +import { ScrollView, View } from 'react-native'; export default function AskHello() { const { t } = useTranslation(); return ( - - {/* 内容区域 IP与介绍文本*/} - - - {t('ask.hi', { ns: 'ask' })} - {"\n"} - {t('ask.iAmMemo', { ns: 'ask' })} - - - - - {t('ask.ready', { ns: 'ask' })} - {"\n"} - {t('ask.justAsk', { ns: 'ask' })} - - + + + + + {t('ask.hi', { ns: 'ask' })} + {"\n"} + {t('ask.iAmMemo', { ns: 'ask' })} + + + + + + {t('ask.ready', { ns: 'ask' })} + {"\n"} + {t('ask.justAsk', { ns: 'ask' })} + + + ); } \ No newline at end of file diff --git a/components/ask/send.tsx b/components/ask/send.tsx index 4de8c84..b53e037 100644 --- a/components/ask/send.tsx +++ b/components/ask/send.tsx @@ -1,7 +1,8 @@ 'use client'; import VoiceSvg from '@/assets/icons/svg/vioce.svg'; -import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'; import { + Keyboard, StyleSheet, TextInput, TouchableOpacity, @@ -12,7 +13,7 @@ import { fetchApi } from '@/lib/server-api-util'; import { Message } from '@/types/ask'; interface Props { - setIsHello: (isHello: boolean) => void, + setIsHello: Dispatch>, conversationId: string | null, setUserMessages: Dispatch>; setConversationId: (conversationId: string) => void, @@ -70,7 +71,6 @@ export default function SendMessage(props: Props) { // 如果没有对话ID,创建新对话并获取消息,否则直接获取消息 if (!conversationId) { createNewConversation(text); - setIsHello(false); } else { getConversation({ session_id: conversationId, @@ -81,6 +81,26 @@ export default function SendMessage(props: Props) { setInputValue(''); } } + useEffect(() => { + const keyboardWillShowListener = Keyboard.addListener( + 'keyboardWillShow', + () => { + console.log('Keyboard will show'); + setIsHello(false); + setUserMessages([{ + content: { + text: "快来寻找你的记忆吧。。。" + }, + role: 'Assistant', + timestamp: new Date().toISOString() + }]) + } + ); + + return () => { + keyboardWillShowListener.remove(); + }; + }, []); return ( diff --git a/components/layout/ask.tsx b/components/layout/ask.tsx index 80aa069..6016e77 100644 --- a/components/layout/ask.tsx +++ b/components/layout/ask.tsx @@ -2,10 +2,13 @@ import NavbarSvg from "@/assets/icons/svg/navbar.svg"; import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import React from 'react'; -import { Platform, TouchableOpacity, View } from 'react-native'; +import { Dimensions, Platform, TouchableOpacity, View } from 'react-native'; import { Circle, Ellipse, G, Mask, Path, Rect, Svg } from 'react-native-svg'; const AskNavbar = () => { + // 获取设备尺寸 + const { width } = Dimensions.get('window'); + return ( { params: { newSession: "true" } }); }} - className={`${Platform.OS === 'web' ? '-mt-[4rem]' : '-mt-[5rem] ml-[0.8rem]'}`} + className={`${Platform.OS === 'web' ? '-mt-[4rem]' : width <= 375 ? '-mt-[5rem] ml-[2rem]' : '-mt-[5rem] ml-[0.8rem]'}`} >