Compare commits
No commits in common. "4a30e7f43c1f954a7d1871c7aa21841714df7fd3" and "b7d9186570bdf247ebc4f31a40128748a470f324" have entirely different histories.
4a30e7f43c
...
b7d9186570
1
app.json
1
app.json
@ -84,7 +84,6 @@
|
|||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"API_ENDPOINT": "http://192.168.31.107:8081/api",
|
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "04721dd4-6b15-495a-b9ec-98187c613172"
|
"projectId": "04721dd4-6b15-495a-b9ec-98187c613172"
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { PermissionProvider } from '@/context/PermissionContext';
|
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic';
|
import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic';
|
||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
@ -8,6 +7,7 @@ import { StatusBar } from 'expo-status-bar';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
import '../global.css';
|
import '../global.css';
|
||||||
|
import { PermissionProvider } from '@/context/PermissionContext';
|
||||||
import { Provider } from "../provider";
|
import { Provider } from "../provider";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
@ -34,15 +34,15 @@ export default function RootLayout() {
|
|||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<Provider>
|
<Provider>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="login"
|
name="login"
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
animation: 'fade'
|
animation: 'fade'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Provider>
|
</Provider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
|
|||||||
269
components/ask/aiChat.tsx
Normal file
269
components/ask/aiChat.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
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<React.SetStateAction<{ visible: boolean, data: Video | MaterialItem }>>;
|
||||||
|
modalVisible: { visible: boolean, data: Video | MaterialItem };
|
||||||
|
setModalDetailsVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, content: any }>>;
|
||||||
|
modalDetailsVisible: { visible: boolean, content: any };
|
||||||
|
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
selectedImages: string[];
|
||||||
|
t: TFunction;
|
||||||
|
setCancel: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
cancel: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => {
|
||||||
|
const isUser = item.role === 'User';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={`flex-row items-start gap-2 w-full ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||||
|
{!isUser && <ChatSvg width={36} height={36} />}
|
||||||
|
<View className="max-w-[90%] mb-[1rem] flex flex-col gap-2 ">
|
||||||
|
<View style={{ width: "100%", flexDirection: "row", alignItems: "flex-end" }}>
|
||||||
|
<View
|
||||||
|
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble rounded-2xl'} border-0 ${!isUser && (item.content.video_material_infos && item.content.video_material_infos.length > 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 }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View className={`${isUser ? 'bg-bgPrimary' : 'bg-aiBubble'}`}>
|
||||||
|
<Text style={isUser ? styles.userText : styles.aiText}>
|
||||||
|
{!isUser
|
||||||
|
? item.content.text == "keepSearchIng"
|
||||||
|
? <Loading />
|
||||||
|
: item.content.text
|
||||||
|
: item.content.text
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{(mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.length || 0 > 0) && (
|
||||||
|
<View className="relative">
|
||||||
|
<View style={[styles.imageGridContainer, { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }]}>
|
||||||
|
{mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image) => (
|
||||||
|
<Pressable
|
||||||
|
key={image?.id || image?.video?.id}
|
||||||
|
onPress={() => {
|
||||||
|
setModalVisible({ visible: true, data: image });
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '32%',
|
||||||
|
aspectRatio: 1,
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContextMenu
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
svg: <DownloadSvg width={20} height={20} />,
|
||||||
|
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: <CancelSvg width={20} height={20} color='red' />,
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 12,
|
||||||
|
}}
|
||||||
|
resizeMode="cover"
|
||||||
|
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
{
|
||||||
|
((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3
|
||||||
|
&& <TouchableOpacity className="absolute top-1/2 -translate-y-1/2 -right-4 translate-x-1/2 bg-bgPrimary flex flex-row items-center gap-2 p-1 pl-2 rounded-full" onPress={() => {
|
||||||
|
setSelectedImages([])
|
||||||
|
setModalDetailsVisible({ visible: true, content: item.content });
|
||||||
|
}}>
|
||||||
|
<ThemedText className="!text-white font-semibold">{((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0))}</ThemedText>
|
||||||
|
<View className="bg-white rounded-full p-2">
|
||||||
|
<MoreSvg />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{
|
||||||
|
item.content.text == "keepSearchIng"
|
||||||
|
&&
|
||||||
|
<Text style={{ color: "d9d9d9" }}>
|
||||||
|
{t("ask:ask.think")}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
{/* {item.askAgain && item.askAgain.length > 0 && (
|
||||||
|
<View className={`mr-10`}>
|
||||||
|
{item.askAgain.map((suggestion, index, array) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={suggestion.id}
|
||||||
|
className={`bg-yellow-50 rounded-xl px-4 py-2 border border-yellow-200 border-0 mb-2 ${index === array.length - 1 ? 'mb-0 rounded-b-3xl rounded-t-2xl' : 'rounded-2xl'}`}
|
||||||
|
>
|
||||||
|
<Text className="text-gray-700">{suggestion.text}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)} */}
|
||||||
|
{/* 单个图片弹窗 */}
|
||||||
|
<SingleContentModel modalVisible={modalVisible} setModalVisible={setModalVisible} />
|
||||||
|
{/* 全部图片详情弹窗 */}
|
||||||
|
<SelectModel
|
||||||
|
modalDetailsVisible={modalDetailsVisible}
|
||||||
|
setModalDetailsVisible={setModalDetailsVisible}
|
||||||
|
insets={insets}
|
||||||
|
setSelectedImages={setSelectedImages}
|
||||||
|
selectedImages={selectedImages}
|
||||||
|
t={t}
|
||||||
|
setCancel={setCancel}
|
||||||
|
cancel={cancel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import MessageItem from '../chat/message-item/message-item';
|
import MessageItem from './aiChat';
|
||||||
|
|
||||||
// 继承 FlatListProps 来接收所有 FlatList 的属性
|
// 继承 FlatListProps 来接收所有 FlatList 的属性
|
||||||
interface ChatProps extends Omit<FlatListProps<Message>, 'data' | 'renderItem'> {
|
interface ChatProps extends Omit<FlatListProps<Message>, 'data' | 'renderItem'> {
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { ContentPart } from "@/types/ask";
|
import { Video } from "@/types/ask";
|
||||||
|
import { MaterialItem } from "@/types/personal-info";
|
||||||
import { Image, Modal, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { Image, Modal, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import VideoPlayer from "./VideoPlayer";
|
import VideoPlayer from "./VideoPlayer";
|
||||||
|
|
||||||
interface SingleContentModelProps {
|
interface SingleContentModelProps {
|
||||||
modalVisible: { visible: boolean, data: ContentPart };
|
modalVisible: { visible: boolean, data: Video | MaterialItem };
|
||||||
setModalVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, data: ContentPart }>>;
|
setModalVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, data: Video | MaterialItem }>>;
|
||||||
}
|
}
|
||||||
const SingleContentModel = ({ modalVisible, setModalVisible }: SingleContentModelProps) => {
|
const SingleContentModel = ({ modalVisible, setModalVisible }: SingleContentModelProps) => {
|
||||||
const isVideo = (data: ContentPart) => {
|
const isVideo = (data: Video | MaterialItem): data is Video => {
|
||||||
return data.type === 'video';
|
return 'video' in data;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -17,16 +18,16 @@ const SingleContentModel = ({ modalVisible, setModalVisible }: SingleContentMode
|
|||||||
transparent={true}
|
transparent={true}
|
||||||
visible={modalVisible.visible}
|
visible={modalVisible.visible}
|
||||||
onRequestClose={() => {
|
onRequestClose={() => {
|
||||||
setModalVisible({ visible: false, data: {} as ContentPart });
|
setModalVisible({ visible: false, data: {} as Video | MaterialItem });
|
||||||
}}>
|
}}>
|
||||||
<View style={styles.centeredView}>
|
<View style={styles.centeredView}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.background}
|
style={styles.background}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModalVisible({ visible: false, data: {} as ContentPart })
|
setModalVisible({ visible: false, data: {} as Video | MaterialItem })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity style={styles.modalView} onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })}>
|
<TouchableOpacity style={styles.modalView} onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}>
|
||||||
{isVideo(modalVisible.data) ? (
|
{isVideo(modalVisible.data) ? (
|
||||||
// 视频播放器
|
// 视频播放器
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -37,28 +38,28 @@ const SingleContentModel = ({ modalVisible, setModalVisible }: SingleContentMode
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
maxHeight: "60%",
|
maxHeight: "60%",
|
||||||
}}
|
}}
|
||||||
onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })}
|
onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}
|
||||||
>
|
>
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
videoUrl={modalVisible.data.url || ""}
|
videoUrl={modalVisible.data.video.file_info.url}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })}
|
onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
// 图片预览
|
// 图片预览
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
onPress={() => setModalVisible({ visible: false, data: {} as ContentPart })}
|
onPress={() => setModalVisible({ visible: false, data: {} as Video | MaterialItem })}
|
||||||
style={styles.imageContainer}
|
style={styles.imageContainer}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: modalVisible.data.url }}
|
source={{ uri: modalVisible.data.preview_file_info?.url }}
|
||||||
style={styles.fullWidthImage}
|
style={styles.fullWidthImage}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
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<React.SetStateAction<{ visible: boolean, data: ContentPart }>>;
|
|
||||||
setCancel: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
cancel: boolean;
|
|
||||||
t: TFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaGridProps) => {
|
|
||||||
// 只取前6个元素(2行,每行3个)
|
|
||||||
const displayItems = mediaItems.slice(0, 6);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="flex-row flex-wrap justify-between">
|
|
||||||
{displayItems.map((media) => (
|
|
||||||
<Pressable
|
|
||||||
key={media.id}
|
|
||||||
onPress={() => {
|
|
||||||
setModalVisible({ visible: true, data: media });
|
|
||||||
}}
|
|
||||||
className="mb-2 w-[32%] aspect-square"
|
|
||||||
>
|
|
||||||
<ContextMenu
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
svg: <DownloadSvg width={20} height={20} />,
|
|
||||||
label: t("ask:ask.save"),
|
|
||||||
onPress: () => {
|
|
||||||
if (media?.url) {
|
|
||||||
saveMediaToGallery(media?.url, t);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textStyle: { color: '#4C320C' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
svg: <CancelSvg width={20} height={20} color='red' />,
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri: media?.url }}
|
|
||||||
className="w-full h-full rounded-xl"
|
|
||||||
resizeMode="cover"
|
|
||||||
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
|
|
||||||
/>
|
|
||||||
</ContextMenu>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default MediaGrid;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
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<React.SetStateAction<{ visible: boolean, data: any }>>;
|
|
||||||
setCancel: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
cancel: boolean;
|
|
||||||
t: any;
|
|
||||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
setModalDetailsVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, content: any }>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageBubble = ({
|
|
||||||
item,
|
|
||||||
isUser,
|
|
||||||
setModalVisible,
|
|
||||||
setCancel,
|
|
||||||
cancel,
|
|
||||||
t,
|
|
||||||
setSelectedImages,
|
|
||||||
setModalDetailsVisible
|
|
||||||
}: MessageBubbleProps) => {
|
|
||||||
return (
|
|
||||||
<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-4 py-3`}
|
|
||||||
style={{ marginRight: getMessageText(item) == "keepSearchIng" ? 0 : isUser ? 0 : 10 }}
|
|
||||||
>
|
|
||||||
<MessageContent
|
|
||||||
item={item}
|
|
||||||
isUser={isUser}
|
|
||||||
setModalVisible={setModalVisible}
|
|
||||||
setCancel={setCancel}
|
|
||||||
cancel={cancel}
|
|
||||||
t={t}
|
|
||||||
setSelectedImages={setSelectedImages}
|
|
||||||
setModalDetailsVisible={setModalDetailsVisible}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MessageBubble;
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
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<React.SetStateAction<{ visible: boolean, data: ContentPart }>>;
|
|
||||||
setCancel: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
cancel: boolean;
|
|
||||||
t: TFunction;
|
|
||||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
setModalDetailsVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, content: any }>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageContent = ({
|
|
||||||
item,
|
|
||||||
isUser,
|
|
||||||
setModalVisible,
|
|
||||||
setCancel,
|
|
||||||
cancel,
|
|
||||||
t,
|
|
||||||
setSelectedImages,
|
|
||||||
setModalDetailsVisible
|
|
||||||
}: MessageContentProps) => {
|
|
||||||
return (
|
|
||||||
<View className={`${isUser ? 'bg-bgPrimary' : 'bg-aiBubble'}`}>
|
|
||||||
<Text className={`text-base ${isUser ? 'text-[#4C320C]' : 'text-black'} leading-6 font-semibold`}>
|
|
||||||
{!isUser
|
|
||||||
? getMessageText(item) == "keepSearchIng"
|
|
||||||
? <Loading />
|
|
||||||
: getMessageText(item)
|
|
||||||
: getMessageText(item)
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{isMessageContainMedia(item) && (
|
|
||||||
<View className="relative">
|
|
||||||
{item.content instanceof Array && (() => {
|
|
||||||
const mediaItems = item.content.filter((media: ContentPart) => media.type !== 'text');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="mt-2">
|
|
||||||
<MediaGrid
|
|
||||||
mediaItems={mediaItems}
|
|
||||||
setModalVisible={setModalVisible}
|
|
||||||
setCancel={setCancel}
|
|
||||||
cancel={cancel}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
{
|
|
||||||
(item.content instanceof Array && item.content.length > 3)
|
|
||||||
&& <TouchableOpacity className="absolute top-1/2 -translate-y-1/2 -right-4 translate-x-1/2 bg-bgPrimary flex flex-row items-center gap-2 p-1 pl-2 rounded-full" onPress={() => {
|
|
||||||
setSelectedImages([])
|
|
||||||
setModalDetailsVisible({ visible: true, content: item.content });
|
|
||||||
}}>
|
|
||||||
<ThemedText className="!text-white font-semibold">{item.content.length}</ThemedText>
|
|
||||||
<View className="bg-white rounded-full p-2">
|
|
||||||
<MoreSvg />
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MessageContent;
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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<React.SetStateAction<{ visible: boolean, data: any }>>;
|
|
||||||
setCancel: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
cancel: boolean;
|
|
||||||
t: any;
|
|
||||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
setModalDetailsVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, content: any }>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageRow = ({
|
|
||||||
item,
|
|
||||||
isUser,
|
|
||||||
setModalVisible,
|
|
||||||
setCancel,
|
|
||||||
cancel,
|
|
||||||
t,
|
|
||||||
setSelectedImages,
|
|
||||||
setModalDetailsVisible
|
|
||||||
}: MessageRowProps) => {
|
|
||||||
return (
|
|
||||||
<View className="w-full flex-row items-end">
|
|
||||||
<MessageBubble
|
|
||||||
item={item}
|
|
||||||
isUser={isUser}
|
|
||||||
setModalVisible={setModalVisible}
|
|
||||||
setCancel={setCancel}
|
|
||||||
cancel={cancel}
|
|
||||||
t={t}
|
|
||||||
setSelectedImages={setSelectedImages}
|
|
||||||
setModalDetailsVisible={setModalDetailsVisible}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
getMessageText(item) == "keepSearchIng"
|
|
||||||
&&
|
|
||||||
<Text className="text-[#d9d9d9]">
|
|
||||||
{t("ask:ask.think")}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MessageRow;
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
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<React.SetStateAction<{ visible: boolean, data: ContentPart }>>;
|
|
||||||
modalVisible: { visible: boolean, data: ContentPart };
|
|
||||||
setModalDetailsVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, content: any }>>;
|
|
||||||
modalDetailsVisible: { visible: boolean, content: any };
|
|
||||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
selectedImages: string[];
|
|
||||||
t: TFunction;
|
|
||||||
setCancel: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
cancel: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => {
|
|
||||||
const isUser = item.role === User;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className={`flex-row items-start gap-2 w-full ${isUser ? 'justify-end' : 'justify-start'}`}>
|
|
||||||
{!isUser && <ChatSvg width={36} height={36} />}
|
|
||||||
<View className="max-w-[90%] mb-[1rem] flex flex-col gap-2 ">
|
|
||||||
<MessageRow
|
|
||||||
item={item}
|
|
||||||
isUser={isUser}
|
|
||||||
setModalVisible={setModalVisible}
|
|
||||||
setCancel={setCancel}
|
|
||||||
cancel={cancel}
|
|
||||||
t={t}
|
|
||||||
setSelectedImages={setSelectedImages}
|
|
||||||
setModalDetailsVisible={setModalDetailsVisible}
|
|
||||||
/>
|
|
||||||
{/* {item.askAgain && item.askAgain.length > 0 && (
|
|
||||||
<View className={`mr-10`}>
|
|
||||||
{item.askAgain.map((suggestion, index, array) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={suggestion.id}
|
|
||||||
className={`bg-yellow-50 rounded-xl px-4 py-2 border border-yellow-200 border-0 mb-2 ${index === array.length - 1 ? 'mb-0 rounded-b-3xl rounded-t-2xl' : 'rounded-2xl'}`}
|
|
||||||
>
|
|
||||||
<Text className="text-gray-700">{suggestion.text}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)} */}
|
|
||||||
{/* 单个图片弹窗 */}
|
|
||||||
<SingleContentModel modalVisible={modalVisible} setModalVisible={setModalVisible} />
|
|
||||||
{/* 全部图片详情弹窗 */}
|
|
||||||
<SelectModel
|
|
||||||
modalDetailsVisible={modalDetailsVisible}
|
|
||||||
setModalDetailsVisible={setModalDetailsVisible}
|
|
||||||
insets={insets}
|
|
||||||
setSelectedImages={setSelectedImages}
|
|
||||||
selectedImages={selectedImages}
|
|
||||||
t={t}
|
|
||||||
setCancel={setCancel}
|
|
||||||
cancel={cancel}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MessageItem;
|
|
||||||
|
|
||||||
@ -128,7 +128,6 @@ export const fetchApi = async <T>(
|
|||||||
needToast = true,
|
needToast = true,
|
||||||
needToken = true,
|
needToken = true,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
// console.log("API_ENDPOINT", Constants.expoConfig?.extra?.API_ENDPOINT);
|
|
||||||
const makeRequest = async (isRetry = false): Promise<ApiResponse<T>> => {
|
const makeRequest = async (isRetry = false): Promise<ApiResponse<T>> => {
|
||||||
try {
|
try {
|
||||||
let token = "";
|
let token = "";
|
||||||
|
|||||||
44
types/ask.ts
44
types/ask.ts
@ -1,3 +1,4 @@
|
|||||||
|
import { MaterialItem } from "./personal-info";
|
||||||
|
|
||||||
interface FileInfo {
|
interface FileInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@ -33,44 +34,21 @@ export interface Video {
|
|||||||
video: VideoInfo;
|
video: VideoInfo;
|
||||||
video_clips: VideoClip[];
|
video_clips: VideoClip[];
|
||||||
}
|
}
|
||||||
export interface ContentPart {
|
export interface Content {
|
||||||
type: string;
|
text: string;
|
||||||
text?: string;
|
image_material_infos?: MaterialItem[];
|
||||||
caption?: string;
|
video_material_infos?: Video[];
|
||||||
url?: string;
|
|
||||||
id?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
id: string;
|
content: Content;
|
||||||
content: string | ContentPart[];
|
role: 'User' | 'Assistant'; // 使用联合类型限制 role 的值
|
||||||
role: typeof User | typeof Assistant;
|
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
// askAgain?: Array<{
|
askAgain?: Array<{
|
||||||
// id: string;
|
id: string;
|
||||||
// text: 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 {
|
export interface Chat {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user