fix: attachments

This commit is contained in:
Vukasin 2025-06-26 22:45:38 +02:00
parent 4d0a937331
commit 3e73f01054
8 changed files with 146 additions and 37 deletions

View File

@ -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();

View File

@ -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>
);

View File

@ -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={

View File

@ -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([]);
}

View File

@ -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,

View File

@ -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',
},

View File

@ -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);

View File

@ -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
};
}
/**