mirror of https://github.com/kortix-ai/suna.git
fix: attachments
This commit is contained in:
parent
4d0a937331
commit
3e73f01054
|
@ -993,6 +993,19 @@ export const initiateAgent = async (
|
|||
}
|
||||
): Promise<{ thread_id: string; agent_run_id: string }> => {
|
||||
console.log('[API] initiateAgent called with message:', message.substring(0, 50) + '...');
|
||||
console.log('[API] initiateAgent files:', options?.files?.length || 0);
|
||||
|
||||
if (options?.files?.length) {
|
||||
console.log('[API] File details:');
|
||||
options.files.forEach((file, index) => {
|
||||
console.log(`[API] File ${index}:`, {
|
||||
name: file.name,
|
||||
localUri: file.localUri,
|
||||
type: file.type,
|
||||
size: file.size
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const supabase = createSupabaseClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
@ -1013,17 +1026,22 @@ export const initiateAgent = async (
|
|||
|
||||
// Add files to FormData if provided
|
||||
if (options?.files?.length) {
|
||||
options.files.forEach((file) => {
|
||||
console.log('[API] Adding files to FormData...');
|
||||
options.files.forEach((file, index) => {
|
||||
const normalizedName = file.name || file.fileName || 'unknown_file';
|
||||
console.log(`[API] Adding file ${index} to FormData:`, normalizedName);
|
||||
|
||||
formData.append('files', {
|
||||
uri: file.localUri || file.uri,
|
||||
name: normalizedName,
|
||||
type: file.type || file.mimeType || 'application/octet-stream',
|
||||
} as any, normalizedName);
|
||||
});
|
||||
console.log('[API] All files added to FormData');
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[API] Sending request to /agent/initiate...');
|
||||
const response = await fetch(`${SERVER_URL}/agent/initiate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -1032,8 +1050,13 @@ export const initiateAgent = async (
|
|||
body: formData,
|
||||
});
|
||||
|
||||
console.log('[API] Response status:', response.status);
|
||||
console.log('[API] Response headers:', JSON.stringify(Object.fromEntries(response.headers.entries())));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const errorText = await response.text();
|
||||
console.error('[API] Error response body:', errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
|
|
@ -103,6 +103,7 @@ export const AttachmentGroup: React.FC<AttachmentGroupProps> = ({
|
|||
isUploading={isUploadedFile ? attachment.isUploading : false}
|
||||
uploadError={isUploadedFile ? attachment.uploadError : undefined}
|
||||
uploadedBlob={isUploadedFile ? attachment.cachedBlob : undefined}
|
||||
localUri={isUploadedFile ? attachment.localUri : undefined}
|
||||
/>
|
||||
{onRemove && (
|
||||
<TouchableOpacity
|
||||
|
@ -141,6 +142,7 @@ export const AttachmentGroup: React.FC<AttachmentGroupProps> = ({
|
|||
isUploading={isUploadedFile ? attachment.isUploading : false}
|
||||
uploadError={isUploadedFile ? attachment.uploadError : undefined}
|
||||
uploadedBlob={isUploadedFile ? attachment.cachedBlob : undefined}
|
||||
localUri={isUploadedFile ? attachment.localUri : undefined}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { commonStyles } from '@/constants/CommonStyles';
|
|||
import { useChatSession, useNewChatSession } from '@/hooks/useChatHooks';
|
||||
import { useThemedStyles } from '@/hooks/useThemeColor';
|
||||
import { useIsNewChatMode, useSelectedProject } from '@/stores/ui-store';
|
||||
import { UploadedFile } from '@/utils/file-upload';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Keyboard, KeyboardEvent, Platform, View } from 'react-native';
|
||||
import { ChatInput } from './ChatInput';
|
||||
|
@ -141,10 +142,16 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ className }) => {
|
|||
/>
|
||||
</View>
|
||||
<ChatInput
|
||||
onSendMessage={(content: string) => {
|
||||
// For new chat mode, we can pass files in the first message
|
||||
// Files are already handled within ChatInput for uploads
|
||||
sendMessage(content);
|
||||
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);
|
||||
}
|
||||
}}
|
||||
onCancelStream={stopAgent}
|
||||
placeholder={
|
||||
|
|
|
@ -16,7 +16,7 @@ import Animated, {
|
|||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
interface ChatInputProps {
|
||||
onSendMessage: (message: string) => void;
|
||||
onSendMessage: (message: string, files?: UploadedFile[]) => void;
|
||||
onAttachPress?: () => void;
|
||||
onMicPress?: () => void;
|
||||
onCancelStream?: () => void;
|
||||
|
@ -78,16 +78,17 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
|||
if (message.trim() || attachedFiles.length > 0) {
|
||||
let finalMessage = message.trim();
|
||||
|
||||
// Add file attachments to message
|
||||
if (attachedFiles.length > 0) {
|
||||
// For existing projects with sandboxId, add file references to message
|
||||
// For new chat mode, let server handle file references to avoid duplicates
|
||||
if (attachedFiles.length > 0 && sandboxId) {
|
||||
const fileInfo = attachedFiles
|
||||
.map(file => `[Uploaded File: ${file.path}]`)
|
||||
.join('\n');
|
||||
finalMessage = finalMessage ? `${finalMessage}\n\n${fileInfo}` : fileInfo;
|
||||
}
|
||||
|
||||
// Pass the message to the handler
|
||||
onSendMessage(finalMessage);
|
||||
// Pass the message and files separately to the handler
|
||||
onSendMessage(finalMessage, attachedFiles);
|
||||
setMessage('');
|
||||
setAttachedFiles([]);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ interface FileAttachmentProps {
|
|||
isUploading?: boolean;
|
||||
uploadError?: string;
|
||||
uploadedBlob?: Blob; // For optimistic caching
|
||||
localUri?: string; // For React Native local file URIs
|
||||
}
|
||||
|
||||
const getFileIcon = (type: FileType) => {
|
||||
|
@ -55,7 +56,8 @@ export const FileAttachment: React.FC<FileAttachmentProps> = ({
|
|||
layout = 'inline',
|
||||
isUploading = false,
|
||||
uploadError,
|
||||
uploadedBlob
|
||||
uploadedBlob,
|
||||
localUri
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const filename = filepath.split('/').pop() || 'file';
|
||||
|
@ -82,6 +84,9 @@ export const FileAttachment: React.FC<FileAttachmentProps> = ({
|
|||
uploadedBlob
|
||||
);
|
||||
|
||||
// For local files without sandboxId, use localUri directly
|
||||
const localImageUri = !sandboxId && isImage && localUri ? localUri : null;
|
||||
|
||||
const handlePress = () => {
|
||||
onPress?.(filepath);
|
||||
};
|
||||
|
@ -101,7 +106,61 @@ export const FileAttachment: React.FC<FileAttachmentProps> = ({
|
|||
const maxHeight = isGrid ? 200 : 54;
|
||||
const minHeight = isGrid ? 120 : 54;
|
||||
|
||||
// Loading state
|
||||
// For local files (no sandboxId), use blob URL directly
|
||||
if (!sandboxId && localImageUri) {
|
||||
if (isGrid) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.sidebar,
|
||||
borderColor: theme.border,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
aspectRatio: 1.5,
|
||||
maxHeight: 250,
|
||||
minHeight: 150,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.imageGridTouchable}
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: localImageUri }}
|
||||
style={styles.imageGridPreview}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: theme.sidebar,
|
||||
borderColor: theme.border,
|
||||
},
|
||||
styles.imageInlineContainer
|
||||
]}
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: localImageUri }}
|
||||
style={styles.imageInlinePreview}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Loading state (only for server images)
|
||||
if (imageLoading && sandboxId) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
@ -278,6 +337,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
gridContainer: {
|
||||
width: '100%',
|
||||
minWidth: 170,
|
||||
},
|
||||
imageInlineContainer: {
|
||||
height: 54,
|
||||
|
|
|
@ -43,7 +43,7 @@ const MessageItem = memo<MessageItemProps>(({ message, sandboxId, isStreaming, s
|
|||
|
||||
// Apply file parsing to the already-cleaned content from parseMessage
|
||||
const { attachments, cleanContent: fileCleanContent } = useMemo(() =>
|
||||
parseFileAttachments(parsedMessage.cleanContent), [parsedMessage.cleanContent]);
|
||||
parseFileAttachments(parsedMessage.cleanContent, message.metadata?.cached_files), [parsedMessage.cleanContent, message.metadata?.cached_files]);
|
||||
|
||||
const displayContent = useMemo(() => {
|
||||
if (isStreaming && streamProcessed) {
|
||||
|
@ -530,8 +530,8 @@ const styles = StyleSheet.create({
|
|||
fontStyle: 'italic',
|
||||
},
|
||||
thinkingContainer: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 24,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 12,
|
||||
paddingHorizontal: 0,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
|
|
|
@ -682,6 +682,8 @@ export const useNewChatSession = () => {
|
|||
|
||||
const sendMessage = useCallback(async (content: string, files?: any[]) => {
|
||||
try {
|
||||
console.log('[useNewChatSession] sendMessage called with files:', files?.length || 0);
|
||||
|
||||
if (!isInitialized) {
|
||||
// IMMEDIATELY set temp project BEFORE any async operations
|
||||
console.log('[useNewChatSession] Setting temp project IMMEDIATELY');
|
||||
|
@ -703,7 +705,9 @@ export const useNewChatSession = () => {
|
|||
type: 'user',
|
||||
is_llm_message: false,
|
||||
content: content,
|
||||
metadata: {},
|
||||
metadata: {
|
||||
cached_files: files || [] // Store cached files in metadata
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
@ -714,7 +718,7 @@ export const useNewChatSession = () => {
|
|||
|
||||
if (!threadId) {
|
||||
// First message - initiate new chat
|
||||
console.log('[useNewChatSession] Initiating API call for new chat');
|
||||
console.log('[useNewChatSession] Initiating API call for new chat with files:', files?.length || 0);
|
||||
|
||||
const result = await initiateAgent(content, {
|
||||
stream: true,
|
||||
|
@ -722,6 +726,7 @@ export const useNewChatSession = () => {
|
|||
files: files // Pass files to initiateAgent
|
||||
});
|
||||
|
||||
console.log('[useNewChatSession] API call successful, thread_id:', result.thread_id);
|
||||
setThreadId(result.thread_id);
|
||||
setIsSending(false);
|
||||
|
||||
|
|
|
@ -34,27 +34,38 @@ export function getFileUrl(sandboxId: string | undefined, path: string): string
|
|||
}
|
||||
|
||||
/**
|
||||
* Parse file attachments from already-cleaned message content using the pattern: [Uploaded File: /path]
|
||||
* This should be applied AFTER the main message parsing has extracted clean content
|
||||
* Parse file attachments from message content and return clean content + attachments
|
||||
* Now supports using cached files from message metadata
|
||||
*/
|
||||
export function parseFileAttachments(cleanContent: string): ParsedFileContent {
|
||||
if (!cleanContent || typeof cleanContent !== 'string') {
|
||||
return { attachments: [], cleanContent: cleanContent || '' };
|
||||
export function parseFileAttachments(
|
||||
content: string,
|
||||
cachedFiles?: any[]
|
||||
): { attachments: (string | any)[], cleanContent: string } {
|
||||
const fileRegex = /\[Uploaded File: ([^\]]+)\]/g;
|
||||
const matches = [];
|
||||
let match;
|
||||
|
||||
while ((match = fileRegex.exec(content)) !== null) {
|
||||
matches.push(match[1]);
|
||||
}
|
||||
|
||||
// Extract file attachments using regex
|
||||
const attachmentsMatch = cleanContent.match(/\[Uploaded File: (.*?)\]/g);
|
||||
const attachments = attachmentsMatch
|
||||
? attachmentsMatch.map(match => {
|
||||
const pathMatch = match.match(/\[Uploaded File: (.*?)\]/);
|
||||
return pathMatch ? pathMatch[1] : null;
|
||||
}).filter(Boolean) as string[]
|
||||
: [];
|
||||
|
||||
// Clean message content by removing file references
|
||||
const finalCleanContent = cleanContent.replace(/\[Uploaded File: .*?\]/g, '').trim();
|
||||
|
||||
return { attachments, cleanContent: finalCleanContent };
|
||||
|
||||
// Remove file references from content
|
||||
const cleanContent = content.replace(fileRegex, '').trim();
|
||||
|
||||
// If we have cached files, use them instead of just file paths
|
||||
if (cachedFiles && cachedFiles.length > 0) {
|
||||
console.log('[parseFileAttachments] Using cached files:', cachedFiles.length);
|
||||
return {
|
||||
attachments: cachedFiles, // Return UploadedFile objects with localUri/cachedBlob
|
||||
cleanContent
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to file paths for server loading
|
||||
return {
|
||||
attachments: matches,
|
||||
cleanContent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue