Compare commits

...

33 Commits

Author SHA1 Message Date
c6be061130 f 2025-08-09 14:58:54 +08:00
60152a64f0 fix 2025-08-09 14:50:38 +08:00
9de8c3b5c7 f 2025-08-09 13:25:56 +08:00
4c4360cefc test 2025-08-09 13:13:18 +08:00
76f2e6ed48 s 2025-08-09 11:43:36 +08:00
384e607fe1 c 2025-08-09 11:33:16 +08:00
a7b6aeeb31 f 2025-08-09 11:12:42 +08:00
4e755b8f10 f 2025-08-09 11:04:16 +08:00
797414e78b f 2025-08-09 10:52:17 +08:00
2ff82495ac fix: Image 2025-08-09 10:43:18 +08:00
0482f23d97 f 2025-08-09 10:28:23 +08:00
85d9b823de 注释askhello 2025-08-09 10:12:28 +08:00
027f7b1672 fix 2025-08-08 14:29:43 +08:00
8f0cb0ada2 f 2025-08-08 14:13:43 +08:00
1891f5c359 fix 2025-08-07 19:55:46 +08:00
f8bd3b13be fix 2025-08-07 19:50:35 +08:00
1a28d8becd fix: hello回 2025-08-07 19:48:46 +08:00
3f2b849db2 fix: 恢复聊天页面 2025-08-07 19:42:49 +08:00
995f5ad981 f 2025-08-07 19:37:15 +08:00
9341a1560f f 2025-08-07 19:36:26 +08:00
f2b09cb013 f 2025-08-07 19:29:53 +08:00
827bf7b164 fix 2025-08-07 19:22:55 +08:00
fd5ea7f318 chore: 修复一下导航布局 2025-08-07 19:14:46 +08:00
162f3b91e4 fix: 注释掉ask页面的prefetch 2025-08-07 19:08:39 +08:00
b1031cf2b6 fix: 修复一下 2025-08-07 19:04:46 +08:00
45a3660ab8 fix: 试一下navigate 2025-08-07 19:02:17 +08:00
ce50710818 fix: 试一下replace 2025-08-07 19:00:45 +08:00
448e8dfb53 fix 2025-08-07 18:48:41 +08:00
d59378c2da fix: router 2025-08-07 18:43:33 +08:00
15fc8f3ad4 fix 2025-08-07 18:42:54 +08:00
da0b949ca4 fix: 连一下ws 2025-08-07 18:41:49 +08:00
da00968586 fix: ENV 2025-08-07 18:31:43 +08:00
193084fb62 fix: 移除ask页面的ws调用 2025-08-07 17:19:15 +08:00
10 changed files with 186 additions and 233 deletions

View File

@ -1,4 +1,3 @@
import { HapticTab } from '@/components/HapticTab';
import AskNavbar from '@/components/layout/ask';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { requestNotificationPermission } from '@/components/owner/utils';
@ -188,28 +187,13 @@ export default function TabLayout() {
return (
<>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: 'absolute',
},
default: {},
}),
}}
tabBar={props => <AskNavbar {...props} wsStatus={wsStatus} />}
>
{/* 落地页 */}
<Tabs.Screen
name="index"
options={{
title: 'Memo',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
href: null,
}}
/>
{/* 登录 */}
@ -260,10 +244,7 @@ export default function TabLayout() {
<Tabs.Screen
name="ask"
options={{
title: 'ask',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
href: null,
...TransitionPresets.ShiftTransition,
}}
/>
@ -271,10 +252,7 @@ export default function TabLayout() {
<Tabs.Screen
name="memo-list"
options={{
title: 'memo-list',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
href: null,
...TransitionPresets.ShiftTransition,
}}
/>
@ -282,10 +260,7 @@ export default function TabLayout() {
<Tabs.Screen
name="owner"
options={{
title: 'owner',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
href: null,
...TransitionPresets.ShiftTransition,
}}
/>
@ -377,7 +352,6 @@ export default function TabLayout() {
}}
/>
</Tabs >
<AskNavbar wsStatus={wsStatus} />
</>
);
}

View File

@ -1,12 +1,9 @@
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 { getWebSocketErrorMessage, webSocketManager, WsMessage } from "@/lib/websocket-util";
import { Assistant, Message } from "@/types/ask";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { Message } from "@/types/ask";
import { useFocusEffect, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from "react-i18next";
import {
@ -25,6 +22,7 @@ import { runOnJS } from 'react-native-reanimated';
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AskScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const chatListRef = useRef<FlatList>(null);
@ -99,88 +97,88 @@ export default function AskScreen() {
useFocusEffect(
useCallback(() => {
webSocketManager.connect();
// webSocketManager.connect();
const handleChatStream = (message: WsMessage) => {
if (message.type === 'ChatStream') {
setUserMessages(prevMessages => {
const newMessages = [...prevMessages];
const lastMessage = newMessages[newMessages.length - 1];
// const handleChatStream = (message: WsMessage) => {
// if (message.type === 'ChatStream') {
// setUserMessages(prevMessages => {
// const newMessages = [...prevMessages];
// const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === Assistant) {
if (typeof lastMessage.content === 'string') {
if (lastMessage.content === 'keepSearchIng') {
// 第一次收到流式消息,替换占位符
lastMessage.content = message.chunk;
} else {
// 持续追加流式消息
lastMessage.content += message.chunk;
}
} else {
// 如果 content 是数组,则更新第一个 text 部分
const textPart = lastMessage.content.find(p => p.type === 'text');
if (textPart) {
textPart.text = (textPart.text || '') + message.chunk;
}
}
}
return newMessages;
});
}
};
// if (lastMessage && lastMessage.role === Assistant) {
// if (typeof lastMessage.content === 'string') {
// if (lastMessage.content === 'keepSearchIng') {
// // 第一次收到流式消息,替换占位符
// lastMessage.content = message.chunk;
// } else {
// // 持续追加流式消息
// lastMessage.content += message.chunk;
// }
// } else {
// // 如果 content 是数组,则更新第一个 text 部分
// const textPart = lastMessage.content.find(p => p.type === 'text');
// if (textPart) {
// textPart.text = (textPart.text || '') + message.chunk;
// }
// }
// }
// return newMessages;
// });
// }
// };
const handleChatStreamEnd = (message: WsMessage) => {
if (message.type === 'ChatStreamEnd') {
setUserMessages(prevMessages => {
const newMessages = [...prevMessages];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === Assistant) {
// 使用最终消息替换流式消息,确保 message.message 存在
if (message.message) {
newMessages[newMessages.length - 1] = message.message as Message;
} else {
// 如果最终消息为空,则移除 'keepSearchIng' 占位符
return prevMessages.filter(m => !(typeof m.content === 'string' && m.content === 'keepSearchIng'));
}
}
return newMessages;
});
}
};
// const handleChatStreamEnd = (message: WsMessage) => {
// if (message.type === 'ChatStreamEnd') {
// setUserMessages(prevMessages => {
// const newMessages = [...prevMessages];
// const lastMessage = newMessages[newMessages.length - 1];
// if (lastMessage && lastMessage.role === Assistant) {
// // 使用最终消息替换流式消息,确保 message.message 存在
// if (message.message) {
// newMessages[newMessages.length - 1] = message.message as Message;
// } else {
// // 如果最终消息为空,则移除 'keepSearchIng' 占位符
// return prevMessages.filter(m => !(typeof m.content === 'string' && m.content === 'keepSearchIng'));
// }
// }
// return newMessages;
// });
// }
// };
const handleError = (message: WsMessage) => {
if (message.type === 'Error') {
console.log(`WebSocket Error: ${message.code} - ${message.message}`);
// 可以在这里添加错误提示,例如替换最后一条消息为错误信息
setUserMessages(prev => {
// 创建新的数组和新的消息对象
return prev.map((msg, index) => {
if (index === prev.length - 1 &&
typeof msg.content === 'string' &&
msg.content === 'keepSearchIng') {
// 返回新的消息对象
return {
...msg,
content: getWebSocketErrorMessage(message.code, t)
};
}
return msg;
});
});
}
};
// const handleError = (message: WsMessage) => {
// if (message.type === 'Error') {
// console.log(`WebSocket Error: ${message.code} - ${message.message}`);
// // 可以在这里添加错误提示,例如替换最后一条消息为错误信息
// setUserMessages(prev => {
// // 创建新的数组和新的消息对象
// return prev.map((msg, index) => {
// if (index === prev.length - 1 &&
// typeof msg.content === 'string' &&
// msg.content === 'keepSearchIng') {
// // 返回新的消息对象
// return {
// ...msg,
// content: getWebSocketErrorMessage(message.code, t)
// };
// }
// return msg;
// });
// });
// }
// };
webSocketManager.subscribe('ChatStream', handleChatStream);
webSocketManager.subscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.subscribe('Error', handleError);
// webSocketManager.subscribe('ChatStream', handleChatStream);
// webSocketManager.subscribe('ChatStreamEnd', handleChatStreamEnd);
// webSocketManager.subscribe('Error', handleError);
return () => {
webSocketManager.unsubscribe('ChatStream', handleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
webSocketManager.unsubscribe('Error', handleError);
// 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度
// webSocketManager.disconnect();
};
// return () => {
// webSocketManager.unsubscribe('ChatStream', handleChatStream);
// webSocketManager.unsubscribe('ChatStreamEnd', handleChatStreamEnd);
// webSocketManager.unsubscribe('Error', handleError);
// // 可以在这里选择断开连接,或者保持连接以加快下次进入页面的速度
// // webSocketManager.disconnect();
// };
}, [])
);
@ -196,7 +194,7 @@ export default function AskScreen() {
setIsHello(true);
setConversationId(null);
}
}, [sessionId, newSession]);
}, [sessionId, newSession])
useEffect(() => {
if (isHello) {
@ -234,23 +232,6 @@ export default function AskScreen() {
}
}, [isHello, fadeAnim, fadeAnimChat]);
useEffect(() => {
if (!isHello) {
// 不再自动关闭键盘,让用户手动控制
// 这里可以添加其他需要在隐藏hello界面时执行的逻辑
scrollToEnd(false);
}
}, [isHello]);
useFocusEffect(
useCallback(() => {
if (!sessionId) {
setIsHello(true);
setUserMessages([])
}
}, [sessionId])
);
return (
<GestureDetector gesture={gesture}>
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
@ -268,7 +249,9 @@ export default function AskScreen() {
console.log('失去焦点失败:', error);
}
Keyboard.dismiss();
router.push('/memo-list');
setTimeout(() => {
router.replace('/memo-list');
}, 100);
}}
>
<ReturnArrow />
@ -303,7 +286,7 @@ export default function AskScreen() {
}
]}
>
<Chat
{/* <Chat
ref={chatListRef}
userMessages={userMessages}
sessionId={sessionId}
@ -313,7 +296,7 @@ export default function AskScreen() {
contentContainerStyle={styles.chatContentContainer}
showsVerticalScrollIndicator={false}
onContentSizeChange={() => scrollToEnd()}
/>
/> */}
</Animated.View>
</View>
@ -322,14 +305,14 @@ export default function AskScreen() {
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={0} >
<View style={styles.inputContainer} key={conversationId}>
<SendMessage
{/* <SendMessage
setIsHello={setIsHello}
conversationId={conversationId}
setConversationId={setConversationId}
setUserMessages={setUserMessages}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
/>
/> */}
</View>
</KeyboardAvoidingView>
</View >

View File

@ -225,7 +225,7 @@ export default function HomeScreen() {
useEffect(() => {
setIsLoading(true);
checkAuthStatus(router, () => {
router.replace('/ask')
router.replace('/memo-list')
}, false).then(() => {
setIsLoading(false);
}).catch(() => {

View File

@ -14,7 +14,7 @@ import SkeletonItem from '@/components/memo/SkeletonItem';
// 类型定义
import { useUploadManager } from '@/hooks/useUploadManager';
import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch';
import { getCachedData, prefetchChatDetail } from '@/lib/prefetch';
import { fetchApi } from '@/lib/server-api-util';
import { Chat, getMessageText } from '@/types/ask';
import { useTranslation } from 'react-i18next';
@ -105,18 +105,11 @@ const MemoList = () => {
const initialize = async () => {
try {
// 并行预加载资源和数据
// 并行预加载资源和数据
await Promise.all([
preloadAssets(),
prefetchChats().then((data) => {
if (isActive && data) {
setHistoryList(data as Chat[]);
}
}),
fetchHistoryList()
]);
// 主数据加载
await fetchHistoryList();
} catch (error) {
console.error('初始化失败:', error);
} finally {

View File

@ -1,9 +1,9 @@
import { ThemedText } from "@/components/ThemedText";
import { webSocketManager } from "@/lib/websocket-util";
import { Message } from "@/types/ask";
import { Dispatch, SetStateAction } from "react";
import { Dispatch, SetStateAction, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Dimensions, Image, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Image, ScrollView, StyleSheet, TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { createNewConversation } from "./utils";
interface AskHelloProps {
@ -13,10 +13,15 @@ interface AskHelloProps {
}
export default function AskHello({ setUserMessages, setConversationId, setIsHello }: AskHelloProps) {
const { t } = useTranslation();
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
const { width, height } = useWindowDimensions();
const handleCase = async (text: string) => {
const inFlightRef = useRef(false);
const handleCase = useCallback(async (text: string) => {
if (inFlightRef.current) return;
inFlightRef.current = true;
try {
// UI
setIsHello(false);
setUserMessages([
{
@ -34,18 +39,37 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell
]);
const sessionId = await createNewConversation(text);
if (sessionId) {
if (!sessionId) {
console.error("Failed to create a new conversation.");
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
return;
}
setConversationId(sessionId);
webSocketManager.send({
try {
if (webSocketManager && typeof (webSocketManager as any).send === 'function') {
(webSocketManager as any).send({
type: 'Chat',
session_id: sessionId,
message: text
});
} else {
console.error("Failed to create a new conversation.");
throw new Error('WebSocket manager is not ready');
}
} catch (wsErr) {
console.error('WebSocket send failed:', wsErr);
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
}
} catch (err) {
console.error('handleCase failed:', err);
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
} finally {
inFlightRef.current = false;
}
}, [setConversationId, setIsHello, setUserMessages]);
return (
<View className="flex-1 bg-white w-full">
<ScrollView
@ -106,7 +130,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
gap: 8,
width: '100%',
marginTop: 16
},
@ -115,6 +138,8 @@ const styles = StyleSheet.create({
borderColor: "#AC7E35",
borderRadius: 10,
paddingHorizontal: 8,
marginHorizontal: 4,
marginVertical: 4,
width: 'auto',
fontSize: 14,
color: "#4C320C"

View File

@ -3,7 +3,6 @@ import { Message } from "@/types/ask";
import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';
import { TFunction } from "i18next";
import { useCallback } from "react";
import { Alert } from 'react-native';
// 实现一个函数,从两个数组中轮流插入新数组
@ -19,12 +18,12 @@ export const mergeArrays = (arr1: any[], arr2: any[]) => {
// 创建新对话并获取消息
export const createNewConversation = useCallback(async (user_text: string) => {
export const createNewConversation = async (user_text: string) => {
const data = await fetchApi<string>("/chat/new", {
method: "POST",
});
return data
}, []);
};
// 获取对话信息
export const getConversation = async ({

View File

@ -3,8 +3,9 @@ import ChatNotInSvg from "@/assets/icons/svg/chatNotIn.svg";
import PersonInSvg from "@/assets/icons/svg/personIn.svg";
import PersonNotInSvg from "@/assets/icons/svg/personNotIn.svg";
import { WebSocketStatus } from "@/lib/websocket-util";
import { router, usePathname } from "expo-router";
import React, { useCallback, useEffect, useMemo } from 'react';
import { BottomTabBarProps } from "@react-navigation/bottom-tabs";
import { router } from "expo-router";
import React, { useMemo } from 'react';
import { Dimensions, Image, StyleSheet, TouchableOpacity, View } from 'react-native';
import Svg, { Circle, Ellipse, G, Mask, Path, Rect } from "react-native-svg";
@ -42,14 +43,15 @@ const CenterButtonSvg = React.memo(() => (
</Svg>
));
interface AskNavbarProps {
type AskNavbarProps = BottomTabBarProps & {
wsStatus: WebSocketStatus;
}
};
const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
const AskNavbar = ({ state, descriptors, navigation, wsStatus }: AskNavbarProps) => {
// 获取设备尺寸
const { width } = useMemo(() => Dimensions.get('window'), []);
const pathname = usePathname();
const { routes, index } = state;
const currentRouteName = routes[index].name;
const statusColor = useMemo(() => {
switch (wsStatus) {
@ -64,34 +66,6 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
}
}, [wsStatus]);
// 预加载目标页面
useEffect(() => {
const preloadPages = async () => {
try {
await Promise.all([
router.prefetch('/memo-list'),
router.prefetch('/ask'),
router.prefetch('/owner')
]);
} catch (error) {
console.warn('预加载页面失败:', error);
}
};
preloadPages();
}, []);
// 使用 useCallback 缓存导航函数
const navigateTo = useCallback((route: string) => {
if (route === '/ask') {
router.push({
pathname: '/ask',
params: { newSession: "true" }
});
} else {
router.push(route as any);
}
}, []);
// 使用 useMemo 缓存样式对象
const styles = useMemo(() => StyleSheet.create({
container: {
@ -156,7 +130,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
}), [width, statusColor]);
// 如果当前路径是ask页面则不渲染导航栏
if (pathname != '/memo-list' && pathname != '/owner') {
if (currentRouteName !== 'memo-list' && currentRouteName !== 'owner') {
return null;
}
@ -165,18 +139,18 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: width * 1.18, height: 100, resizeMode: 'cover', marginLeft: -width * 0.07 }} />
<View style={styles.navContainer}>
<TouchableOpacity
onPress={() => navigateTo('/memo-list')}
onPress={() => navigation.navigate('memo-list')}
style={[styles.navButton, { alignItems: "flex-start", paddingLeft: 16 }]}
>
<TabIcon
isActive={pathname === "/memo-list"}
isActive={currentRouteName === "memo-list"}
ActiveIcon={ChatInSvg}
InactiveIcon={ChatNotInSvg}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => navigateTo('/ask')}
onPress={() => router.push({ pathname: '/ask', params: { newSession: "true" } })}
style={styles.centerButton}
>
<View style={styles.statusIndicator} />
@ -184,11 +158,11 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
</TouchableOpacity>
<TouchableOpacity
onPress={() => navigateTo('/owner')}
onPress={() => navigation.navigate('owner')}
style={styles.navButton}
>
<TabIcon
isActive={pathname === "/owner"}
isActive={currentRouteName === "owner"}
ActiveIcon={PersonInSvg}
InactiveIcon={PersonNotInSvg}
/>

View File

@ -1,5 +1,10 @@
import React from 'react';
import { View } from 'react-native';
// This is a shim for web and Android where the tab bar is generally opaque.
export default undefined;
export default function TabBarBackground() {
return <View style={{ flex: 1, backgroundColor: 'transparent' }} />;
}
export function useBottomTabOverflow() {
return 0;

View File

@ -25,7 +25,7 @@ export interface PagedResult<T> {
// 获取.env文件中的变量
export const API_ENDPOINT = Constants.expoConfig?.extra?.API_ENDPOINT || "http://192.168.31.16:31646/api";
export const API_ENDPOINT = process.env.EXPO_PUBLIC_API_ENDPOINT || Constants.expoConfig?.extra?.API_ENDPOINT;
// 更新 access_token 的逻辑 - 用于React组件中

View File

@ -4,7 +4,7 @@ import { TFunction } from 'i18next';
import { Platform } from 'react-native';
// 从环境变量或默认值中定义 WebSocket 端点
export const WEBSOCKET_ENDPOINT = Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT || "ws://192.168.31.16:31646/ws/chat";
export const WEBSOCKET_ENDPOINT = process.env.EXPO_PUBLIC_WEBSOCKET_ENDPOINT || Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT;
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';