feat: ask页面样式

This commit is contained in:
jinyaqiu 2025-08-04 14:32:28 +08:00
parent c406312e0a
commit b266b6646d
10 changed files with 107 additions and 37 deletions

View File

@ -4,10 +4,11 @@ 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 { webSocketManager, WsMessage } from "@/lib/websocket-util";
import { getWebSocketErrorMessage, webSocketManager, WsMessage } from "@/lib/websocket-util";
import { Assistant, Message } from "@/types/ask";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from "react-i18next";
import {
Animated,
FlatList,
@ -31,6 +32,7 @@ export default function AskScreen() {
const [selectedImages, setSelectedImages] = useState<string[]>([]);
const fadeAnim = useRef(new Animated.Value(1)).current;
const fadeAnimChat = useRef(new Animated.Value(0)).current;
const { t } = useTranslation();
const { sessionId, newSession } = useLocalSearchParams<{
sessionId: string;
@ -132,16 +134,23 @@ export default function AskScreen() {
const handleError = (message: WsMessage) => {
if (message.type === 'Error') {
console.error(`WebSocket Error: ${message.code} - ${message.message}`);
console.log(`WebSocket Error: ${message.code} - ${message.message}`);
// 可以在这里添加错误提示,例如替换最后一条消息为错误信息
setUserMessages(prev => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && typeof lastMessage.content === 'string' && lastMessage.content === 'keepSearchIng') {
lastMessage.content = `Error: ${message.message}`;
}
return newMessages;
})
// 创建新的数组和新的消息对象
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;
});
});
}
};

View File

@ -1,7 +1,9 @@
import ConversationsSvg from '@/assets/icons/svg/conversations.svg';
import StoriesSvg from '@/assets/icons/svg/stories.svg';
import CarouselComponent from '@/components/owner/carousel';
import CreateCountComponent from '@/components/owner/createCount';
import Ranking from '@/components/owner/ranking';
import MemberCard from '@/components/owner/rights/memberCard';
import SettingModal from '@/components/owner/setting';
import SkeletonOwner from '@/components/owner/SkeletonOwner';
@ -126,9 +128,9 @@ export default function OwnerPage() {
<MemberCard pro={userInfoDetails?.membership_level} />
{/* 分类 */}
{/* <View style={{ marginHorizontal: -16, marginBottom: -16 }}>
<View style={{ marginHorizontal: -16, marginBottom: -16 }}>
<CarouselComponent data={userInfoDetails?.material_counter} />
</View> */}
</View>
{/* 作品数据 */}
<View className='flex flex-row justify-between gap-[1rem]'>
@ -137,7 +139,7 @@ export default function OwnerPage() {
</View>
{/* 排行榜 */}
{/* <Ranking data={userInfoDetails.title_rankings} /> */}
<Ranking data={userInfoDetails.title_rankings} />
</View>
}
// 优化性能:添加 getItemLayout

View File

@ -1,5 +1,5 @@
import { ContentPart, Message } from '@/types/ask';
import React, { Dispatch, ForwardedRef, forwardRef, SetStateAction, useCallback, useMemo, useState } from 'react';
import React, { Dispatch, ForwardedRef, forwardRef, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
FlatList,
@ -12,6 +12,8 @@ import MessageItem from '../chat/message-item/message-item';
import SelectModel from "./selectModel";
import SingleContentModel from "./singleContentModel";
// 继承 FlatListProps 来接收所有 FlatList 的属性
interface ChatProps extends Omit<FlatListProps<Message>, 'data' | 'renderItem'> {
userMessages: Message[];
@ -37,6 +39,18 @@ function ChatComponent(
}), []);
const [modalDetailsVisible, setModalDetailsVisible] = useState<{ visible: boolean, content: any }>({ visible: false, content: [] });
const flatListRef = useRef<FlatList>(null);
const prevMessagesLength = useRef(0);
// 自动滚动到底部
useEffect(() => {
if (userMessages.length > 0 && userMessages.length !== prevMessagesLength.current) {
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
prevMessagesLength.current = userMessages.length;
}
}, [userMessages.length]);
const renderMessageItem = useCallback(({ item, index }: { item: Message, index: number }) => {
const itemStyle = index === 0 ? { marginTop: 16, marginHorizontal: 16 } : { marginHorizontal: 16 };
@ -63,7 +77,17 @@ function ChatComponent(
return (
<SafeAreaView style={{ flex: 1 }}>
<FlatList
ref={ref}
ref={(node) => {
// 处理转发 ref 和内部 ref
if (ref) {
if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
}
flatListRef.current = node;
}}
data={userMessages}
keyExtractor={keyExtractor}
renderItem={renderMessageItem}
@ -75,7 +99,17 @@ function ChatComponent(
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={11}
{...restProps} // 将所有其他属性传递给 FlatList
onContentSizeChange={() => {
if (userMessages.length > 0) {
flatListRef.current?.scrollToEnd({ animated: true });
}
}}
onLayout={() => {
if (userMessages.length > 0) {
flatListRef.current?.scrollToEnd({ animated: false });
}
}}
{...restProps}
/>
{/* 单个图片弹窗 */}
<SingleContentModel modalVisible={modalVisible} setModalVisible={setModalVisible} />

View File

@ -100,7 +100,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 8,
padding: 16,
},
dot: {
width: 8,

View File

@ -1,7 +1,7 @@
import React from 'react';
import { View, Text } from 'react-native';
import MessageBubble from './MessageBubble';
import { getMessageText } from "@/types/ask";
import React from 'react';
import { Text, View } from 'react-native';
import MessageBubble from './MessageBubble';
interface MessageRowProps {
item: any;
@ -14,19 +14,19 @@ interface MessageRowProps {
setModalDetailsVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, content: any }>>;
}
const MessageRow = ({
item,
isUser,
setModalVisible,
setCancel,
cancel,
t,
setSelectedImages,
const MessageRow = ({
item,
isUser,
setModalVisible,
setCancel,
cancel,
t,
setSelectedImages,
setModalDetailsVisible
}: MessageRowProps) => {
return (
<View className="w-full flex-row items-end">
<MessageBubble
<View className="w-full" style={{ flexDirection: getMessageText(item) == "keepSearchIng" ? 'row' : 'column', alignItems: getMessageText(item) == "keepSearchIng" ? 'flex-end' : 'flex-start', gap: getMessageText(item) == "keepSearchIng" ? 8 : 0 }}>
<MessageBubble
item={item}
isUser={isUser}
setModalVisible={setModalVisible}

View File

@ -1,9 +1,8 @@
import ChatSvg from "@/assets/icons/svg/chat.svg";
import { ContentPart, getMessageText, isMessageContainMedia, Message, User } from "@/types/ask";
import { ContentPart, Message, User } from "@/types/ask";
import { TFunction } from "i18next";
import React from 'react';
import {
StyleSheet,
View
} from 'react-native';
@ -30,8 +29,8 @@ 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 ">
<MessageRow
<View className="max-w-[85%] mb-[1rem] flex flex-col gap-2">
<MessageRow
item={item}
isUser={isUser}
setModalVisible={setModalVisible}

View File

@ -31,6 +31,11 @@
"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",
"think": "Thinking..."
"think": "Thinking...",
"insufficientPoints": "Insufficient points. Please go to your profile to purchase more points 😊",
"invalidToken": "Invalid token",
"notFound": "Resource not found",
"permissionDenied": "Permission denied",
"notConnected": "Not connected. Please check your network connection or restart the app 😊"
}
}

View File

@ -31,6 +31,11 @@
"introduction3": "想让你的视频内容更吸睛、更有故事感吗?不妨试试从搜索图片入手吧!\n\n你可以先确定视频的主题——是治愈系的自然风景还是复古风的城市街景或是充满活力的生活瞬间然后根据主题去搜索相关的图片比如想做'春日限定'主题,就搜'樱花飘落''草地野餐''嫩芽初绽'之类的画面。\n\n这些图片能帮你快速理清视频的画面脉络甚至能激发新的创意——比如一张老照片里的复古物件或许能延伸出一段关于时光的故事一组星空图片说不定能串联成关于梦想与远方的叙事。把这些图片按你的想法串联起来配上合适的音乐和文案一段有温度的视频就诞生啦试试看吧",
"search": "检索素材",
"video": "创作视频",
"think": "思考中..."
"think": "思考中...",
"insufficientPoints": "积分不足,您可以前往个人中心去购买积分😊",
"invalidToken": "无效的令牌",
"notFound": "未找到资源",
"permissionDenied": "权限不足",
"notConnected": "未连接,请检查网络连接或者退出app重试😊"
}
}

View File

@ -25,7 +25,8 @@ export interface PagedResult<T> {
// 获取.env文件中的变量
export const API_ENDPOINT = Constants.expoConfig?.extra?.API_ENDPOINT || "https://api.memorywake.com/api";
export const API_ENDPOINT = Constants.expoConfig?.extra?.API_ENDPOINT || "http://192.168.31.16:31646/api";
// 更新 access_token 的逻辑 - 用于React组件中
export const useAuthToken = async<T>(message: string | null) => {

View File

@ -1,9 +1,10 @@
import Constants from 'expo-constants';
import * as SecureStore from 'expo-secure-store';
import { TFunction } from 'i18next';
import { Platform } from 'react-native';
// 从环境变量或默认值中定义 WebSocket 端点
export const WEBSOCKET_ENDPOINT = Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT || "wss://api.memorywake.com/ws";
export const WEBSOCKET_ENDPOINT = Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT || "ws://192.168.31.16:31646/ws/chat";
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
@ -225,3 +226,17 @@ class WebSocketManager {
// 导出一个单例,确保整个应用共享同一个 WebSocket 连接
export const webSocketManager = new WebSocketManager();
// webscoket 错误映射
export const getWebSocketErrorMessage = (key: string, t: TFunction) => {
const messages = {
'INSUFFICIENT_POINTS': t('ask:ask.insufficientPoints'),
'INVALID_TOKEN': t('ask:ask.invalidToken'),
'NOT_FOUND': t('ask:ask.notFound'),
'PERMISSION_DENIED': t('ask:ask.permissionDenied'),
'NOT_CONNECTED': t('ask:ask.notConnected'),
};
return messages[key as keyof typeof messages] || t('ask:ask.unknownError');
};