feat: ask优化

This commit is contained in:
jinyaqiu 2025-07-31 20:09:00 +08:00
parent 1941790a05
commit 69735168f7
5 changed files with 228 additions and 100 deletions

View File

@ -18,7 +18,7 @@ import ContextMenu from "../gusture/contextMenu";
import { ThemedText } from "../ThemedText";
import SelectModel from "./selectModel";
import SingleContentModel from "./singleContentModel";
import TypewriterText from "./typewriterText";
import Loading from './threeCircle';
import { mergeArrays, saveMediaToGallery } from "./utils";
interface RenderMessageProps {
@ -42,102 +42,112 @@ const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, set
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={[
styles.messageBubble,
isUser ? styles.userBubble : styles.aiBubble
]}
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble mr-10 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'}`}
>
<View className={`${isUser ? 'bg-bgPrimary' : 'bg-aiBubble'}`}>
<Text style={isUser ? styles.userText : styles.aiText}>
{!isUser
?
sessionId ? item.content.text : <TypewriterText text={item.content.text} speed={100} loop={item.content.text == "正在寻找,请稍等..."} />
: item.content.text
}
</Text>
<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 == "正在寻找,请稍等..." ? 0 : isUser ? 0 : 10 }
]}
>
<View className={`${isUser ? 'bg-bgPrimary' : 'bg-aiBubble'}`}>
<Text style={isUser ? styles.userText : styles.aiText}>
{!isUser
?
sessionId ? item.content.text : item.content.text == "正在寻找,请稍等..." ? <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,
{(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,
}}
>
<Image
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
style={{
width: '100%',
height: '100%',
borderRadius: 12,
<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,
}}
resizeMode="cover"
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
/>
</ContextMenu>
</Pressable>
))}
>
<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>
{
((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 == "正在寻找,请稍等..."
&&
<Text style={{ color: "d9d9d9" }}>
{t("ask:ask.think")}
</Text>
}
</View>
{/* {item.askAgain && item.askAgain.length > 0 && (
<View className={`mr-10`}>
@ -247,10 +257,12 @@ const styles = StyleSheet.create({
userText: {
color: '#4C320C',
fontSize: 16,
lineHeight: 24,
},
aiText: {
color: '#000',
fontSize: 16,
lineHeight: 24,
},
});

View File

@ -170,11 +170,11 @@ export default function SendMessage(props: Props) {
returnKeyType="send"
/>
<TouchableOpacity
style={styles.voiceButton}
style={[styles.voiceButton, { bottom: -10 }]}
onPress={handleSubmit}
className={`absolute right-0 bottom-0`} // 使用绝对定位将按钮放在输入框内右侧
className="absolute right-2"
>
<View style={{ transform: [{ rotate: '330deg' }] }}>
<View>
<SendSvg color={'white'} width={24} height={24} />
</View>
</TouchableOpacity>
@ -204,18 +204,18 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderRadius: 25,
paddingHorizontal: 20,
paddingVertical: 12,
paddingVertical: 13,
fontSize: 16,
width: '100%', // 确保输入框宽度撑满
paddingRight: 50
},
voiceButton: {
width: 40,
height: 40,
padding: 8,
borderRadius: 20,
backgroundColor: '#FF9500',
justifyContent: 'center',
alignItems: 'center',
marginRight: 8, // 添加一点
position: 'absolute',
transform: [{ translateY: -12 }],
},
});

View File

@ -0,0 +1,114 @@
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
const Loading = () => {
// 创建三个动画值,控制每个点的大小变化
const anim1 = useRef(new Animated.Value(0)).current;
const anim2 = useRef(new Animated.Value(0)).current;
const anim3 = useRef(new Animated.Value(0)).current;
// 定义动画序列
const startAnimation = () => {
// 重置动画值
anim1.setValue(0);
anim2.setValue(0);
anim3.setValue(0);
// 创建动画序列
Animated.loop(
Animated.stagger(200, [
// 第一个点动画
Animated.sequence([
Animated.timing(anim1, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(anim1, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]),
// 第二个点动画
Animated.sequence([
Animated.timing(anim2, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(anim2, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]),
// 第三个点动画
Animated.sequence([
Animated.timing(anim3, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(anim3, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]),
])
).start();
};
useEffect(() => {
startAnimation();
return () => {
// 清理动画
anim1.stopAnimation();
anim2.stopAnimation();
anim3.stopAnimation();
};
}, []);
// 颜色插值
const color1 = anim1.interpolate({
inputRange: [0, 0.5, 1],
outputRange: ['#999999', '#4C320C', '#999999'],
});
const color2 = anim2.interpolate({
inputRange: [0, 0.5, 1],
outputRange: ['#999999', '#4C320C', '#999999'],
});
const color3 = anim3.interpolate({
inputRange: [0, 0.5, 1],
outputRange: ['#999999', '#4C320C', '#999999'],
});
return (
<View style={styles.container}>
<Animated.View style={[styles.dot, { backgroundColor: color1 }]} />
<Animated.View style={[styles.dot, { backgroundColor: color2 }]} />
<Animated.View style={[styles.dot, { backgroundColor: color3 }]} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 8,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginHorizontal: 4,
backgroundColor: '#999999',
},
});
export default Loading;

View File

@ -30,6 +30,7 @@
"introduction2": "Looking for the perfect image? Try these search tips for better results:\n\n• Be specific: Try 'autumn forest', 'minimalist desk', or 'vintage poster design'\n\n• Add details: For specific styles, try 'watercolor cat' or 'cyberpunk city nightscape'; for specific uses, try 'royalty-free landscape' or 'commercial use icons'\n\n• Describe the scene: Try 'sunlight through leaves' or 'rainy day coffee shop window'\n\nEnter these keywords, and you might just find the perfect shot!",
"introduction3": "Want to make your videos more engaging and story-driven? Start with an image search!\n\nFirst, decide on your video's theme—whether it's healing natural landscapes, retro cityscapes, or vibrant life moments. Then search for related images. For example, if your theme is 'Spring Limited,' search for 'cherry blossoms falling,' 'picnic in the grass,' or 'first buds of spring.'\n\nThese images can help you visualize your video's flow and even spark new ideas—like how an old photo of a vintage object might inspire a story about time, or how a series of starry sky images could connect into a narrative about dreams and distant places. String these images together with the right music and captions, and you've got yourself a heartwarming video. Give it a try!",
"search": "Search Assets",
"video": "Create Video"
"video": "Create Video",
"think": "Thinking..."
}
}

View File

@ -30,6 +30,7 @@
"introduction2": "想找合适的图片?试试这样搜更精准:\n\n• 明确主题:比如'秋日森林'、'极简风书桌'、'复古海报设计'\n\n• 加上细节:想找特定风格?试试'水彩风猫咪'、'赛博朋克城市夜景';需要特定用途?比如'无版权风景图'、'可商用图标'\n\n• 描述场景:比如'阳光透过树叶的光斑'、'雨天咖啡馆窗外'\n\n输入这些关键词说不定就能找到你想要的画面啦",
"introduction3": "想让你的视频内容更吸睛、更有故事感吗?不妨试试从搜索图片入手吧!\n\n你可以先确定视频的主题——是治愈系的自然风景还是复古风的城市街景或是充满活力的生活瞬间然后根据主题去搜索相关的图片比如想做'春日限定'主题,就搜'樱花飘落''草地野餐''嫩芽初绽'之类的画面。\n\n这些图片能帮你快速理清视频的画面脉络甚至能激发新的创意——比如一张老照片里的复古物件或许能延伸出一段关于时光的故事一组星空图片说不定能串联成关于梦想与远方的叙事。把这些图片按你的想法串联起来配上合适的音乐和文案一段有温度的视频就诞生啦试试看吧",
"search": "检索素材",
"video": "创作视频"
"video": "创作视频",
"think": "思考中..."
}
}