feat: ask页面

This commit is contained in:
jinyaqiu 2025-08-08 17:19:01 +08:00
parent 6efc508fa0
commit d7e07b327a
9 changed files with 69 additions and 57 deletions

View File

@ -57,6 +57,7 @@ export default function AskScreen() {
if (translationX > threshold) { if (translationX > threshold) {
// 从左向右滑动,跳转页面 // 从左向右滑动,跳转页面
runOnJS(router.replace)("/memo-list"); runOnJS(router.replace)("/memo-list");
runOnJS(setConversationId)("")
} }
}) })
.minPointers(1) .minPointers(1)
@ -244,11 +245,12 @@ export default function AskScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
Keyboard.dismiss();
if (!sessionId) { if (!sessionId) {
setIsHello(true); setIsHello(true);
setUserMessages([]) setUserMessages([])
} }
}, [sessionId]) }, [sessionId, Keyboard])
); );
return ( return (

View File

@ -13,6 +13,7 @@ import UploaderProgress from '@/components/file-upload/upload-progress/uploader-
import SkeletonItem from '@/components/memo/SkeletonItem'; import SkeletonItem from '@/components/memo/SkeletonItem';
// 类型定义 // 类型定义
import { Fonts } from '@/constants/Fonts';
import { useUploadManager } from '@/hooks/useUploadManager'; import { useUploadManager } from '@/hooks/useUploadManager';
import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch'; import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch';
import { fetchApi } from '@/lib/server-api-util'; import { fetchApi } from '@/lib/server-api-util';
@ -227,7 +228,7 @@ const MemoList = () => {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<View style={[styles.container, { paddingTop: insets.top }]}> <View style={[styles.container, { paddingTop: insets.top + 8 }]}>
<FlatList <FlatList
ref={flatListRef} ref={flatListRef}
data={historyList} data={historyList}
@ -273,7 +274,7 @@ const styles = StyleSheet.create({
backgroundColor: '#fff', backgroundColor: '#fff',
}, },
headerContainer: { headerContainer: {
paddingBottom: 16, paddingBottom: 8,
backgroundColor: '#fff', backgroundColor: '#fff',
}, },
title: { title: {
@ -281,7 +282,8 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
color: '#4C320C', color: '#4C320C',
textAlign: 'center', textAlign: 'center',
marginBottom: 16, marginBottom: 8,
fontFamily: Fonts["quicksand"]
}, },
listContent: { listContent: {
paddingBottom: Platform.select({ paddingBottom: Platform.select({
@ -314,13 +316,15 @@ const styles = StyleSheet.create({
}, },
memoTitle: { memoTitle: {
fontSize: 16, fontSize: 16,
fontWeight: '500', fontWeight: 'bold',
color: '#4C320C', color: '#4C320C',
marginBottom: 4, marginBottom: 4,
fontFamily: Fonts['sfPro']
}, },
memoSubtitle: { memoSubtitle: {
fontSize: 14, fontSize: 14,
color: '#AC7E35', color: '#AC7E35',
fontFamily: Fonts['inter']
}, },
separator: { separator: {
height: 1 / PixelRatio.get(), height: 1 / PixelRatio.get(),

View File

@ -1,7 +1,7 @@
import { StyleProp, StyleSheet, Text, TextStyle, type TextProps } from 'react-native'; import { StyleProp, StyleSheet, Text, TextStyle, type TextProps } from 'react-native';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import { FontColor, Fonts } from '@/constants/Fonts'; import { FontColor, Fonts, FontSize, FontWeight } from '@/constants/Fonts';
import { useThemeColor } from '@/hooks/useThemeColor'; import { useThemeColor } from '@/hooks/useThemeColor';
export type ThemeColor = keyof typeof Colors.light & keyof typeof Colors.dark; export type ThemeColor = keyof typeof Colors.light & keyof typeof Colors.dark;
@ -11,9 +11,9 @@ export type ThemedTextProps = TextProps & {
lightColor?: string; lightColor?: string;
darkColor?: string; darkColor?: string;
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'sfPro' | 'inter'; type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'sfPro' | 'inter';
weight?: 'regular' | 'medium' | 'semiBold' | 'bold'; weight?: FontWeight;
size?: 'xxs' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'; size?: FontSize;
radius?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'; radius?: FontSize
color?: ThemeColor | FontColor | ColorValue; color?: ThemeColor | FontColor | ColorValue;
}; };

View File

@ -1,9 +1,11 @@
import { ContentPart, Message } from '@/types/ask'; import { ContentPart, Message } from '@/types/ask';
import { useFocusEffect } from 'expo-router';
import React, { Dispatch, ForwardedRef, forwardRef, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { Dispatch, ForwardedRef, forwardRef, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
FlatList, FlatList,
FlatListProps, FlatListProps,
Keyboard,
SafeAreaView, SafeAreaView,
View View
} from 'react-native'; } from 'react-native';
@ -52,6 +54,13 @@ function ChatComponent(
} }
}, [userMessages.length]); }, [userMessages.length]);
useFocusEffect(
useCallback(() => {
Keyboard.dismiss();
}, [Keyboard, sessionId])
);
const renderMessageItem = useCallback(({ item, index }: { item: Message, index: number }) => { const renderMessageItem = useCallback(({ item, index }: { item: Message, index: number }) => {
const itemStyle = index === 0 ? { marginTop: 16, marginHorizontal: 16 } : { marginHorizontal: 16 }; const itemStyle = index === 0 ? { marginTop: 16, marginHorizontal: 16 } : { marginHorizontal: 16 };
return ( return (

View File

@ -1,4 +1,5 @@
import { ThemedText } from "@/components/ThemedText"; import { ThemedText } from "@/components/ThemedText";
import { Fonts } from "@/constants/Fonts";
import { webSocketManager } from "@/lib/websocket-util"; import { webSocketManager } from "@/lib/websocket-util";
import { Message } from "@/types/ask"; import { Message } from "@/types/ask";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
@ -58,15 +59,13 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
<View className="items-center"> <View className="items-center">
<ThemedText style={{ fontSize: 32, fontWeight: 'bold', textAlign: 'center', lineHeight: 40, }}> <ThemedText style={{ textAlign: 'center', lineHeight: 40, }} size="title" weight="bold">
{t('ask.hi', { ns: 'ask' })} {t('ask.hi', { ns: 'ask' })}
{"\n"} {"\n"}
{t('ask.iAmMemo', { ns: 'ask' })} {t('ask.iAmMemo', { ns: 'ask' })}
</ThemedText> </ThemedText>
<View> <Image source={require('@/assets/images/png/icon/ip.png')} style={{ width: width * 0.4, height: height * 0.25 }} />
<Image source={require('@/assets/images/png/icon/ip.png')} style={{ width: width * 0.5, height: height * 0.3 }} /> <ThemedText className="text-center -mt-10" size='base' color="textPrimary" type="sfPro" weight="medium">
</View>
<ThemedText className="!text-textPrimary text-center -mt-10" style={{ fontSize: 16 }}>
{t('ask.ready', { ns: 'ask' })} {t('ask.ready', { ns: 'ask' })}
{"\n"} {"\n"}
{t('ask.justAsk', { ns: 'ask' })} {t('ask.justAsk', { ns: 'ask' })}
@ -112,11 +111,14 @@ const styles = StyleSheet.create({
}, },
case: { case: {
borderWidth: 1, borderWidth: 1,
borderColor: "#AC7E35", borderColor: Fonts["textPrimary"],
borderRadius: 10, borderRadius: 10,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 2,
width: 'auto', width: 'auto',
fontSize: 14, fontSize: Fonts["sm"],
color: "#4C320C" color: Fonts["textSecondary"],
fontFamily: Fonts["sfPro"]
} }
}) })

View File

@ -12,6 +12,7 @@ import {
View View
} from 'react-native'; } from 'react-native';
import { Fonts } from '@/constants/Fonts';
import { webSocketManager, WsMessage } from '@/lib/websocket-util'; import { webSocketManager, WsMessage } from '@/lib/websocket-util';
import { Message } from '@/types/ask'; import { Message } from '@/types/ask';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -190,37 +191,33 @@ export default function SendMessage(props: Props) {
} }
])); ]));
let currentSessionId = conversationId; let currentSessionId = conversationId;
console.log("currentSessionIdcurrentSessionId", currentSessionId);
// 如果没有对话ID先创建一个新对话 // 如果没有对话ID先创建一个新对话
if (!currentSessionId) { if (!currentSessionId) {
const newCurrentSessionId = await createNewConversation(text); const newCurrentSessionId = await createNewConversation(text);
if (newCurrentSessionId) { if (newCurrentSessionId) {
setConversationId(newCurrentSessionId); setConversationId(newCurrentSessionId);
webSocketManager.send({
type: 'Chat',
session_id: newCurrentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
} else { } else {
console.error("无法获取 session_id消息发送失败。"); console.error("无法获取 session_id消息发送失败1。");
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
} }
} }
// 通过 WebSocket 发送消息 // 通过 WebSocket 发送消息
if (currentSessionId) { if (currentSessionId) {
webSocketManager.send({ try {
type: 'Chat', webSocketManager.send({
session_id: currentSessionId, type: 'Chat',
message: text, session_id: currentSessionId,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined, message: text,
}); image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
setSelectedImages([]); });
} else { setSelectedImages([]);
console.error("无法获取 session_id消息发送失败。"); } catch (error) {
// 可以在这里处理错误,例如显示一个提示 console.error("无法获取 session_id消息发送失败2。", error);
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng')); setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
}
} }
// 将输入框清空 // 将输入框清空
setInputValue(''); setInputValue('');
@ -252,16 +249,17 @@ export default function SendMessage(props: Props) {
<ScrollView horizontal={true} style={{ display: isHello ? 'flex' : 'none' }}> <ScrollView horizontal={true} style={{ display: isHello ? 'flex' : 'none' }}>
<TouchableOpacity style={[styles.button, { borderColor: '#FFB645' }]} onPress={() => handleQuitly('search')}> <TouchableOpacity style={[styles.button, { borderColor: '#FFB645' }]} onPress={() => handleQuitly('search')}>
<SunSvg width={18} height={18} /> <SunSvg width={18} height={18} />
<ThemedText>{t("ask:ask.search")}</ThemedText> <ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.search")}</ThemedText>
</TouchableOpacity><TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}> </TouchableOpacity>
<TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
<VideoSvg width={18} height={18} /> <VideoSvg width={18} height={18} />
<ThemedText>{t("ask:ask.video")}</ThemedText> <ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.video")}</ThemedText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Ask MeMo Anything..." placeholder="Ask MeMo Anything..."
placeholderTextColor="#999" placeholderTextColor={Fonts["textPrimary"]}
value={inputValue} value={inputValue}
onChangeText={(text: string) => { onChangeText={(text: string) => {
setInputValue(text); setInputValue(text);
@ -296,25 +294,23 @@ const styles = StyleSheet.create({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
gap: 5, gap: 5,
// backgroundColor: '#F8F8F8'
}, },
container: { container: {
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#transparent', backgroundColor: '#transparent',
}, },
input: { input: {
// borderColor: '#d9d9d9', color: Fonts["textPrimary"],
borderColor: '#AC7E35', borderColor: '#AC7E35',
borderWidth: 1, borderWidth: 1,
// borderRadius: 18, borderRadius: 28,
borderRadius: 25,
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 13, paddingVertical: 16,
lineHeight: 20, lineHeight: 20,
fontSize: 16, fontSize: 16,
width: '100%', // 确保输入框宽度撑满 width: '100%',
paddingRight: 50, paddingRight: 50,
backgroundColor: '#fff', // Required for shadow to show on iOS backgroundColor: '#fff',
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
@ -322,7 +318,6 @@ const styles = StyleSheet.create({
}, },
shadowOpacity: 0.15, shadowOpacity: 0.15,
shadowRadius: 3.84, shadowRadius: 3.84,
// Shadow for Android
elevation: 5, elevation: 5,
}, },
voiceButton: { voiceButton: {

View File

@ -26,7 +26,7 @@ const MessageBubble = ({
}: MessageBubbleProps) => { }: MessageBubbleProps) => {
return ( return (
<View <View
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble rounded-2xl'} border-0 ${!isUser && isMessageContainMedia(item) ? '!rounded-t-3xl !rounded-b-2xl' : '!rounded-3xl'} px-3`} className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble rounded-2xl'} border-0 ${!isUser && isMessageContainMedia(item) ? '!rounded-3xl' : '!rounded-3xl'} px-3`}
style={{ marginRight: getMessageText(item) == "keepSearchIng" ? 0 : isUser ? 0 : 10 }} style={{ marginRight: getMessageText(item) == "keepSearchIng" ? 0 : isUser ? 0 : 10 }}
> >
<MessageContent <MessageContent

View File

@ -4,11 +4,10 @@ import { TFunction } from "i18next";
import React from 'react'; import React from 'react';
import { import {
StyleSheet, StyleSheet,
TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
import { ThemedText } from "@/components/ThemedText"; import { Fonts } from "@/constants/Fonts";
import MessageRow from './MessageRow'; import MessageRow from './MessageRow';
interface RenderMessageProps { interface RenderMessageProps {
@ -24,10 +23,9 @@ interface RenderMessageProps {
t: TFunction; t: TFunction;
setCancel: React.Dispatch<React.SetStateAction<boolean>>; setCancel: React.Dispatch<React.SetStateAction<boolean>>;
cancel: boolean; cancel: boolean;
setUserMessages: React.Dispatch<React.SetStateAction<Message[]>>;
} }
const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages, setUserMessages }: RenderMessageProps) => { const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => {
const isUser = item.role === User; const isUser = item.role === User;
return ( return (
@ -44,7 +42,7 @@ const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, set
setSelectedImages={setSelectedImages} setSelectedImages={setSelectedImages}
setModalDetailsVisible={setModalDetailsVisible} setModalDetailsVisible={setModalDetailsVisible}
/> />
{item.content instanceof Array && item.content.filter((media: ContentPart) => media.type !== 'text').length > 0 && ( {/* {item.content instanceof Array && item.content.filter((media: ContentPart) => media.type !== 'text').length > 0 && (
<View style={styles.tips}> <View style={styles.tips}>
<TouchableOpacity style={[styles.tip, { borderRadius: 16 }]} onPress={() => { <TouchableOpacity style={[styles.tip, { borderRadius: 16 }]} onPress={() => {
@ -55,7 +53,7 @@ const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, set
<ThemedText style={styles.tipText}>Help me find materials for subsequent operations.</ThemedText> <ThemedText style={styles.tipText}>Help me find materials for subsequent operations.</ThemedText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )} */}
</View> </View>
</View> </View>
); );
@ -74,7 +72,8 @@ const styles = StyleSheet.create({
}, },
tipText: { tipText: {
color: '#4C320C', color: '#4C320C',
fontSize: 14 fontSize: 14,
fontFamily: Fonts['inter']
} }
}); });

View File

@ -22,6 +22,7 @@ export const Fonts = {
'3xl': 30, '3xl': 30,
'4xl': 36, '4xl': 36,
'5xl': 48, '5xl': 48,
"title": 32,
// color // color
bgPrimary: '#FFB645', bgPrimary: '#FFB645',
@ -37,6 +38,6 @@ export const Fonts = {
} as const; } as const;
export type FontWeight = keyof Omit<typeof Fonts, 'quicksand' | 'sfPro' | 'inter' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>; export type FontWeight = keyof Omit<typeof Fonts, 'quicksand' | 'sfPro' | 'inter' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>;
export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'; export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | 'title';
export type FontColor = 'bgPrimary' | 'bgSecondary' | 'textPrimary' | 'textSecondary' | 'textThird' | 'textWhite'; export type FontColor = 'bgPrimary' | 'bgSecondary' | 'textPrimary' | 'textSecondary' | 'textThird' | 'textWhite';