suna/apps/mobile/components/ChatContainer.tsx

174 lines
6.3 KiB
TypeScript
Raw Normal View History

2025-06-20 06:43:45 +08:00
import { commonStyles } from '@/constants/CommonStyles';
2025-06-25 05:59:07 +08:00
import { useChatSession, useNewChatSession } from '@/hooks/useChatHooks';
2025-06-20 06:09:25 +08:00
import { useThemedStyles } from '@/hooks/useThemeColor';
2025-06-25 05:59:07 +08:00
import { useIsNewChatMode, useSelectedProject } from '@/stores/ui-store';
2025-06-27 04:45:38 +08:00
import { UploadedFile } from '@/utils/file-upload';
2025-06-21 05:26:22 +08:00
import React, { useEffect, useState } from 'react';
import { Keyboard, KeyboardEvent, Platform, View } from 'react-native';
2025-06-17 04:21:36 +08:00
import { ChatInput } from './ChatInput';
2025-06-20 06:09:25 +08:00
import { MessageThread } from './MessageThread';
2025-06-20 06:43:45 +08:00
import { SkeletonText } from './Skeleton';
2025-06-20 06:09:25 +08:00
import { Body } from './Typography';
2025-06-17 04:21:36 +08:00
interface ChatContainerProps {
2025-06-20 06:09:25 +08:00
className?: string;
2025-06-17 04:21:36 +08:00
}
2025-06-20 06:09:25 +08:00
export const ChatContainer: React.FC<ChatContainerProps> = ({ className }) => {
2025-06-25 05:59:07 +08:00
const selectedProject = useSelectedProject();
const isNewChatMode = useIsNewChatMode();
2025-06-21 05:26:22 +08:00
const [isAtBottomOfChat, setIsAtBottomOfChat] = useState(true);
const [keyboardHeight, setKeyboardHeight] = useState(0);
2025-06-20 06:09:25 +08:00
2025-06-25 05:59:07 +08:00
// Use appropriate chat session based on mode
const projectChatSession = useChatSession(
(!isNewChatMode && selectedProject?.id && selectedProject.id !== 'new-chat-temp')
? selectedProject.id
: ''
);
const newChatSession = useNewChatSession();
// Select the right session based on mode
const chatSession = isNewChatMode ? newChatSession : projectChatSession;
2025-06-20 06:09:25 +08:00
const {
messages,
sendMessage,
stopAgent,
isGenerating,
streamContent,
streamError,
2025-06-25 05:59:07 +08:00
} = chatSession;
// For project mode, we still need these specific loading states
const { isLoadingThread, isLoadingMessages, isSending: projectIsSending } = isNewChatMode ?
{ isLoadingThread: false, isLoadingMessages: false, isSending: false } :
projectChatSession;
// Get the correct isSending state based on mode
const isSending = isNewChatMode ? (newChatSession.isSending || false) : projectIsSending;
2025-06-20 06:09:25 +08:00
2025-06-21 05:26:22 +08:00
// Track keyboard height for MessageThread padding
useEffect(() => {
const handleKeyboardShow = (event: KeyboardEvent) => {
setKeyboardHeight(event.endCoordinates.height);
};
const handleKeyboardHide = () => {
setKeyboardHeight(0);
};
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const showSubscription = Keyboard.addListener(showEvent, handleKeyboardShow);
const hideSubscription = Keyboard.addListener(hideEvent, handleKeyboardHide);
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
2025-06-20 06:09:25 +08:00
const styles = useThemedStyles((theme) => ({
container: {
flex: 1,
backgroundColor: theme.background,
},
loadingContainer: {
2025-06-20 06:43:45 +08:00
...commonStyles.flexCenter,
2025-06-20 06:09:25 +08:00
backgroundColor: theme.background,
2025-06-20 06:43:45 +08:00
paddingHorizontal: 32,
2025-06-20 06:09:25 +08:00
},
emptyContainer: {
2025-06-20 06:43:45 +08:00
...commonStyles.flexCenter,
2025-06-20 06:09:25 +08:00
backgroundColor: theme.background,
paddingHorizontal: 32,
},
emptyText: {
color: theme.mutedForeground,
fontSize: 18,
textAlign: 'center' as const,
lineHeight: 24,
},
emptySubtext: {
color: theme.mutedForeground,
fontSize: 14,
textAlign: 'center' as const,
marginTop: 8,
opacity: 0.7,
},
chatContent: {
flex: 1,
},
}));
2025-06-21 05:26:22 +08:00
const handleScrollPositionChange = (isAtBottom: boolean) => {
setIsAtBottomOfChat(isAtBottom);
};
2025-06-25 05:59:07 +08:00
// Show loading state while thread is being fetched (NOT created) - only for project mode
if (!isNewChatMode && selectedProject && isLoadingThread) {
2025-06-17 04:21:36 +08:00
return (
2025-06-20 06:09:25 +08:00
<View style={styles.loadingContainer}>
2025-06-20 06:43:45 +08:00
<SkeletonText lines={3} />
2025-06-17 04:21:36 +08:00
</View>
);
}
2025-06-25 05:59:07 +08:00
// Show empty state when no project is selected - ONLY in project mode
if (!isNewChatMode && !selectedProject) {
2025-06-20 06:09:25 +08:00
return (
<View style={styles.emptyContainer}>
2025-06-20 06:43:45 +08:00
<Body style={styles.emptyText}>Select a project to start chatting</Body>
2025-06-20 06:09:25 +08:00
<Body style={styles.emptySubtext}>
2025-06-20 06:43:45 +08:00
Choose a project from the sidebar to begin your conversation.
2025-06-20 06:09:25 +08:00
</Body>
</View>
);
}
2025-06-17 04:21:36 +08:00
return (
2025-06-17 04:39:00 +08:00
<View style={styles.container}>
2025-06-20 06:09:25 +08:00
<View style={styles.chatContent}>
<MessageThread
messages={messages}
isGenerating={isGenerating}
2025-06-28 17:49:46 +08:00
isSending={isSending}
2025-06-20 06:09:25 +08:00
streamContent={streamContent}
streamError={streamError}
2025-06-20 06:43:45 +08:00
isLoadingMessages={isLoadingMessages}
2025-06-21 05:26:22 +08:00
onScrollPositionChange={handleScrollPositionChange}
keyboardHeight={keyboardHeight}
2025-06-28 18:16:29 +08:00
sandboxId={selectedProject?.sandbox?.id}
2025-06-20 06:09:25 +08:00
/>
</View>
2025-06-17 04:21:36 +08:00
<ChatInput
2025-06-27 04:45:38 +08:00
onSendMessage={(content: string, files?: UploadedFile[]) => {
console.log('[ChatContainer] Sending message with files:', files?.length || 0);
if (isNewChatMode) {
// For new chat mode, pass files to the sendMessage function
(newChatSession.sendMessage as any)(content, files);
} else {
// For existing chat mode, files are already uploaded to sandbox
sendMessage(content);
}
2025-06-20 06:43:45 +08:00
}}
2025-06-25 04:43:44 +08:00
onCancelStream={stopAgent}
2025-06-17 04:21:36 +08:00
placeholder={
isGenerating
2025-06-20 06:09:25 +08:00
? "AI is responding..."
: isSending
? "Sending..."
2025-06-25 05:59:07 +08:00
: isNewChatMode
? "Start a new conversation..."
: `Chat with ${selectedProject?.name || 'project'}...`
2025-06-17 04:21:36 +08:00
}
2025-06-21 05:26:22 +08:00
isAtBottomOfChat={isAtBottomOfChat}
2025-06-25 04:43:44 +08:00
isGenerating={isGenerating}
isSending={isSending}
2025-06-17 04:21:36 +08:00
/>
2025-06-17 04:39:00 +08:00
</View>
2025-06-17 04:21:36 +08:00
);
2025-06-20 06:09:25 +08:00
};