From 4a30e7f43c1f954a7d1871c7aa21841714df7fd3 Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Sun, 3 Aug 2025 16:27:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor::=20=E6=B6=88=E6=81=AF=E4=BD=93?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ask/aiChat.tsx | 269 ------------------ components/ask/chat.tsx | 2 +- components/ask/singleContentModel.tsx | 27 +- components/chat/message-item/MediaGrid.tsx | 81 ++++++ .../chat/message-item/MessageBubble.tsx | 46 +++ .../chat/message-item/MessageContent.tsx | 77 +++++ components/chat/message-item/MessageRow.tsx | 50 ++++ components/chat/message-item/message-item.tsx | 76 +++++ types/ask.ts | 50 +++- 9 files changed, 380 insertions(+), 298 deletions(-) delete mode 100644 components/ask/aiChat.tsx create mode 100644 components/chat/message-item/MediaGrid.tsx create mode 100644 components/chat/message-item/MessageBubble.tsx create mode 100644 components/chat/message-item/MessageContent.tsx create mode 100644 components/chat/message-item/MessageRow.tsx create mode 100644 components/chat/message-item/message-item.tsx diff --git a/components/ask/aiChat.tsx b/components/ask/aiChat.tsx deleted file mode 100644 index 4ec6971..0000000 --- a/components/ask/aiChat.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import CancelSvg from '@/assets/icons/svg/cancel.svg'; -import ChatSvg from "@/assets/icons/svg/chat.svg"; -import DownloadSvg from '@/assets/icons/svg/download.svg'; -import MoreSvg from "@/assets/icons/svg/more.svg"; -import { Message, Video } from "@/types/ask"; -import { MaterialItem } from "@/types/personal-info"; -import { TFunction } from "i18next"; -import React from 'react'; -import { - Image, - Pressable, - StyleSheet, - Text, - TouchableOpacity, - View -} from 'react-native'; -import ContextMenu from "../gusture/contextMenu"; -import { ThemedText } from "../ThemedText"; -import SelectModel from "./selectModel"; -import SingleContentModel from "./singleContentModel"; -import Loading from './threeCircle'; -import { mergeArrays, saveMediaToGallery } from "./utils"; - -interface RenderMessageProps { - insets: { top: number }; - item: Message; - sessionId: string; - setModalVisible: React.Dispatch>; - modalVisible: { visible: boolean, data: Video | MaterialItem }; - setModalDetailsVisible: React.Dispatch>; - modalDetailsVisible: { visible: boolean, content: any }; - setSelectedImages: React.Dispatch>; - selectedImages: string[]; - t: TFunction; - setCancel: React.Dispatch>; - cancel: boolean; -} - -const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => { - const isUser = item.role === 'User'; - - return ( - - {!isUser && } - - - 0 || item.content.image_material_infos && item.content.image_material_infos.length > 0) ? '!rounded-t-3xl !rounded-b-2xl' : '!rounded-3xl'}`} - style={[ - styles.messageBubble, - isUser ? styles.userBubble : styles.aiBubble, - { marginRight: item.content.text == "keepSearchIng" ? 0 : isUser ? 0 : 10 } - ]} - > - - - {!isUser - ? item.content.text == "keepSearchIng" - ? - : item.content.text - : item.content.text - } - - - {(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) => ( - { - setModalVisible({ visible: true, data: image }); - }} - style={{ - width: '32%', - aspectRatio: 1, - marginBottom: 8, - }} - > - , - label: t("ask:ask.save"), - onPress: () => { - const imageUrl = image?.preview_file_info?.url || image.video?.preview_file_info?.url; - if (imageUrl) { - saveMediaToGallery(imageUrl, t); - } - }, - textStyle: { color: '#4C320C' } - }, - { - svg: , - label: t("ask:ask.cancel"), - onPress: () => { - setCancel(true); - }, - textStyle: { color: 'red' } - } - ]} - cancel={cancel} - menuStyle={{ - backgroundColor: 'white', - borderRadius: 8, - padding: 8, - minWidth: 150, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }} - > - - - - ))} - - { - ((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3 - && { - setSelectedImages([]) - setModalDetailsVisible({ visible: true, content: item.content }); - }}> - {((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0))} - - - - - } - - )} - - - { - item.content.text == "keepSearchIng" - && - - {t("ask:ask.think")} - - } - - {/* {item.askAgain && item.askAgain.length > 0 && ( - - {item.askAgain.map((suggestion, index, array) => ( - - {suggestion.text} - - ))} - - )} */} - {/* 单个图片弹窗 */} - - {/* 全部图片详情弹窗 */} - - - - ); -}; - -export default MessageItem; - -const styles = StyleSheet.create({ - imageGridContainer: { - flexDirection: 'row', - flexWrap: 'nowrap', - width: '100%', - marginTop: 8, - }, - video: { - width: '100%', - height: '100%', - }, - imageContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - width: '100%', - maxHeight: '60%', - }, - fullWidthImage: { - width: '100%', - height: "54%", - marginBottom: 8, - }, - gridImage: { - aspectRatio: 1, - marginBottom: 8, - }, - background: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - modalView: { - borderRadius: 20, - alignItems: 'center', - height: '100%', - width: "100%", - justifyContent: 'center', - alignSelf: 'center', - }, - container: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - userAvatar: { - width: 30, - height: 30, - borderRadius: 15, - }, - messageList: { - padding: 16, - }, - messageBubble: { - paddingHorizontal: 16, - paddingVertical: 12, - fontWeight: "600" - }, - userBubble: { - alignSelf: 'flex-end', - backgroundColor: '#FFB645', - marginLeft: 10, - }, - aiBubble: { - alignSelf: 'flex-start', - backgroundColor: '#fff', - marginRight: 10, - borderWidth: 1, - borderColor: '#e5e5ea', - }, - userText: { - color: '#4C320C', - fontSize: 16, - lineHeight: 24, - }, - aiText: { - color: '#000', - fontSize: 16, - lineHeight: 24, - }, -}); - diff --git a/components/ask/chat.tsx b/components/ask/chat.tsx index 69f42d1..e25f315 100644 --- a/components/ask/chat.tsx +++ b/components/ask/chat.tsx @@ -9,7 +9,7 @@ import { View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import MessageItem from './aiChat'; +import MessageItem from '../chat/message-item/message-item'; // 继承 FlatListProps 来接收所有 FlatList 的属性 interface ChatProps extends Omit, 'data' | 'renderItem'> { diff --git a/components/ask/singleContentModel.tsx b/components/ask/singleContentModel.tsx index 4140ae7..6d69f10 100644 --- a/components/ask/singleContentModel.tsx +++ b/components/ask/singleContentModel.tsx @@ -1,15 +1,14 @@ -import { Video } from "@/types/ask"; -import { MaterialItem } from "@/types/personal-info"; +import { ContentPart } from "@/types/ask"; import { Image, Modal, StyleSheet, TouchableOpacity, View } from "react-native"; import VideoPlayer from "./VideoPlayer"; interface SingleContentModelProps { - modalVisible: { visible: boolean, data: Video | MaterialItem }; - setModalVisible: React.Dispatch>; + modalVisible: { visible: boolean, data: ContentPart }; + setModalVisible: React.Dispatch>; } const SingleContentModel = ({ modalVisible, setModalVisible }: SingleContentModelProps) => { - const isVideo = (data: Video | MaterialItem): data is Video => { - return 'video' in data; + const isVideo = (data: ContentPart) => { + return data.type === 'video'; }; return ( @@ -18,16 +17,16 @@ const SingleContentModel = ({ modalVisible, setModalVisible }: SingleContentMode transparent={true} visible={modalVisible.visible} onRequestClose={() => { - setModalVisible({ visible: false, data: {} as Video | MaterialItem }); + setModalVisible({ visible: false, data: {} as ContentPart }); }}> { - setModalVisible({ visible: false, data: {} as Video | MaterialItem }) + setModalVisible({ visible: false, data: {} as ContentPart }) }} /> - setModalVisible({ visible: false, data: {} as Video | MaterialItem })}> + setModalVisible({ visible: false, data: {} as ContentPart })}> {isVideo(modalVisible.data) ? ( // 视频播放器 setModalVisible({ visible: false, data: {} as Video | MaterialItem })} + onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })} > setModalVisible({ visible: false, data: {} as Video | MaterialItem })} + onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })} /> ) : ( // 图片预览 setModalVisible({ visible: false, data: {} as Video | MaterialItem })} + onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })} style={styles.imageContainer} > diff --git a/components/chat/message-item/MediaGrid.tsx b/components/chat/message-item/MediaGrid.tsx new file mode 100644 index 0000000..d9d28f9 --- /dev/null +++ b/components/chat/message-item/MediaGrid.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { View, Pressable, Image, StyleSheet } from 'react-native'; +import ContextMenu from "../../gusture/contextMenu"; +import DownloadSvg from "@/assets/icons/svg/download.svg"; +import CancelSvg from "@/assets/icons/svg/cancel.svg"; +import { ContentPart } from "@/types/ask"; +import { TFunction } from 'i18next'; +import { saveMediaToGallery } from "../../ask/utils"; + +interface MediaGridProps { + mediaItems: ContentPart[]; + setModalVisible: React.Dispatch>; + setCancel: React.Dispatch>; + cancel: boolean; + t: TFunction; +} + +const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaGridProps) => { + // 只取前6个元素(2行,每行3个) + const displayItems = mediaItems.slice(0, 6); + + return ( + + {displayItems.map((media) => ( + { + setModalVisible({ visible: true, data: media }); + }} + className="mb-2 w-[32%] aspect-square" + > + , + label: t("ask:ask.save"), + onPress: () => { + if (media?.url) { + saveMediaToGallery(media?.url, t); + } + }, + textStyle: { color: '#4C320C' } + }, + { + svg: , + label: t("ask:ask.cancel"), + onPress: () => { + setCancel(true); + }, + textStyle: { color: 'red' } + } + ]} + cancel={cancel} + menuStyle={{ + backgroundColor: 'white', + borderRadius: 8, + padding: 8, + minWidth: 150, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }} + > + + + + ))} + + ); +}; + + + +export default MediaGrid; diff --git a/components/chat/message-item/MessageBubble.tsx b/components/chat/message-item/MessageBubble.tsx new file mode 100644 index 0000000..a392c61 --- /dev/null +++ b/components/chat/message-item/MessageBubble.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { View } from 'react-native'; +import MessageContent from './MessageContent'; +import { getMessageText, isMessageContainMedia } from "@/types/ask"; + +interface MessageBubbleProps { + item: any; + isUser: boolean; + setModalVisible: React.Dispatch>; + setCancel: React.Dispatch>; + cancel: boolean; + t: any; + setSelectedImages: React.Dispatch>; + setModalDetailsVisible: React.Dispatch>; +} + +const MessageBubble = ({ + item, + isUser, + setModalVisible, + setCancel, + cancel, + t, + setSelectedImages, + setModalDetailsVisible +}: MessageBubbleProps) => { + return ( + + + + ); +}; + +export default MessageBubble; diff --git a/components/chat/message-item/MessageContent.tsx b/components/chat/message-item/MessageContent.tsx new file mode 100644 index 0000000..dd00e8a --- /dev/null +++ b/components/chat/message-item/MessageContent.tsx @@ -0,0 +1,77 @@ +import MoreSvg from "@/assets/icons/svg/more.svg"; +import { ContentPart, getMessageText, isMessageContainMedia } from "@/types/ask"; +import { TFunction } from 'i18next'; +import React from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; +import Loading from '../../ask/threeCircle'; +import { ThemedText } from "../../ThemedText"; +import MediaGrid from './MediaGrid'; + +interface MessageContentProps { + item: any; + isUser: boolean; + setModalVisible: React.Dispatch>; + setCancel: React.Dispatch>; + cancel: boolean; + t: TFunction; + setSelectedImages: React.Dispatch>; + setModalDetailsVisible: React.Dispatch>; +} + +const MessageContent = ({ + item, + isUser, + setModalVisible, + setCancel, + cancel, + t, + setSelectedImages, + setModalDetailsVisible +}: MessageContentProps) => { + return ( + + + {!isUser + ? getMessageText(item) == "keepSearchIng" + ? + : getMessageText(item) + : getMessageText(item) + } + + + {isMessageContainMedia(item) && ( + + {item.content instanceof Array && (() => { + const mediaItems = item.content.filter((media: ContentPart) => media.type !== 'text'); + + return ( + + + + ); + })()} + { + (item.content instanceof Array && item.content.length > 3) + && { + setSelectedImages([]) + setModalDetailsVisible({ visible: true, content: item.content }); + }}> + {item.content.length} + + + + + } + + )} + + ); +}; + +export default MessageContent; diff --git a/components/chat/message-item/MessageRow.tsx b/components/chat/message-item/MessageRow.tsx new file mode 100644 index 0000000..7898854 --- /dev/null +++ b/components/chat/message-item/MessageRow.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import MessageBubble from './MessageBubble'; +import { getMessageText } from "@/types/ask"; + +interface MessageRowProps { + item: any; + isUser: boolean; + setModalVisible: React.Dispatch>; + setCancel: React.Dispatch>; + cancel: boolean; + t: any; + setSelectedImages: React.Dispatch>; + setModalDetailsVisible: React.Dispatch>; +} + +const MessageRow = ({ + item, + isUser, + setModalVisible, + setCancel, + cancel, + t, + setSelectedImages, + setModalDetailsVisible +}: MessageRowProps) => { + return ( + + + { + getMessageText(item) == "keepSearchIng" + && + + {t("ask:ask.think")} + + } + + ); +}; + +export default MessageRow; diff --git a/components/chat/message-item/message-item.tsx b/components/chat/message-item/message-item.tsx new file mode 100644 index 0000000..2801bb8 --- /dev/null +++ b/components/chat/message-item/message-item.tsx @@ -0,0 +1,76 @@ +import ChatSvg from "@/assets/icons/svg/chat.svg"; +import { ContentPart, getMessageText, isMessageContainMedia, Message, User } from "@/types/ask"; +import { TFunction } from "i18next"; +import React from 'react'; +import { + StyleSheet, + View +} from 'react-native'; +import SelectModel from "../../ask/selectModel"; +import SingleContentModel from "../../ask/singleContentModel"; +import MessageRow from './MessageRow'; + +interface RenderMessageProps { + insets: { top: number }; + item: Message; + sessionId: string; + setModalVisible: React.Dispatch>; + modalVisible: { visible: boolean, data: ContentPart }; + setModalDetailsVisible: React.Dispatch>; + modalDetailsVisible: { visible: boolean, content: any }; + setSelectedImages: React.Dispatch>; + selectedImages: string[]; + t: TFunction; + setCancel: React.Dispatch>; + cancel: boolean; +} + +const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => { + const isUser = item.role === User; + + return ( + + {!isUser && } + + + {/* {item.askAgain && item.askAgain.length > 0 && ( + + {item.askAgain.map((suggestion, index, array) => ( + + {suggestion.text} + + ))} + + )} */} + {/* 单个图片弹窗 */} + + {/* 全部图片详情弹窗 */} + + + + ); +}; + +export default MessageItem; + diff --git a/types/ask.ts b/types/ask.ts index f5365cb..d3164dd 100644 --- a/types/ask.ts +++ b/types/ask.ts @@ -1,4 +1,3 @@ -import { MaterialItem } from "./personal-info"; interface FileInfo { id: string; @@ -34,22 +33,45 @@ export interface Video { video: VideoInfo; video_clips: VideoClip[]; } -export interface Content { - text: string; - image_material_infos?: MaterialItem[]; - video_material_infos?: Video[]; -} -export interface Message { - content: Content; - role: 'User' | 'Assistant'; // 使用联合类型限制 role 的值 - timestamp: string; - askAgain?: Array<{ - id: string; - text: string; - }>; +export interface ContentPart { + type: string; + text?: string; + caption?: string; + url?: string; + id?: string; } +export interface Message { + id: string; + content: string | ContentPart[]; + role: typeof User | typeof Assistant; + timestamp: string; + // askAgain?: Array<{ + // id: string; + // text: string; + // }>; +} + +export function getMessageText(message: Message) { + if (typeof message.content === 'string') { + return message.content; + } else { + return message.content.map((item) => item.text || '').join(''); + } +} + +export function isMessageContainMedia(message: Message) { + if (typeof message.content === 'string') { + return false; + } else { + return message.content.some((item) => item.type === 'image' || item.type === 'video'); + } +} + +export const User = 'user'; +export const Assistant = 'assistant'; + export interface Chat { created_at: string; session_id: string;