diff --git a/frontend/src/app/(dashboard)/agents/_components/agent-builder-chat.tsx b/frontend/src/app/(dashboard)/agents/_components/agent-builder-chat.tsx index 4aff64a7..d120d84e 100644 --- a/frontend/src/app/(dashboard)/agents/_components/agent-builder-chat.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/agent-builder-chat.tsx @@ -18,6 +18,7 @@ import { Check, Clock } from 'lucide-react'; import { BillingError } from '@/lib/api'; import { useQueryClient } from '@tanstack/react-query'; import { agentKeys } from '@/hooks/react-query/agents/keys'; +import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; interface AgentBuilderChatProps { agentId: string; @@ -27,8 +28,8 @@ interface AgentBuilderChatProps { currentStyle: { avatar: string; color: string }; } -export const AgentBuilderChat = React.memo(function AgentBuilderChat({ - agentId, +export const AgentBuilderChat = React.memo(function AgentBuilderChat({ + agentId, formData, handleFieldChange, handleStyleChange, @@ -42,7 +43,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle'); const [isSubmitting, setIsSubmitting] = useState(false); const [hasStartedConversation, setHasStartedConversation] = useState(false); - + const messagesEndRef = useRef(null); const chatInputRef = useRef(null); const previousMessageCountRef = useRef(0); @@ -114,7 +115,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ setThreadId(thread_id); } } - + hasInitiallyLoadedRef.current = true; } else if (chatHistoryQuery.status === 'error') { console.error('[AgentBuilderChat] Error loading chat history:', chatHistoryQuery.error); @@ -125,14 +126,14 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => { setMessages((prev) => { if (!prev) prev = []; - + if (message.type === 'user') { - const optimisticIndex = prev.findIndex(m => - m.message_id.startsWith('temp-user-') && + const optimisticIndex = prev.findIndex(m => + m.message_id.startsWith('temp-user-') && m.content === message.content && m.type === 'user' ); - + if (optimisticIndex !== -1) { console.log(`[AGENT BUILDER] Replacing optimistic message with real message`); const newMessages = [...prev]; @@ -176,8 +177,8 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ }, []); const handleStreamError = useCallback((errorMessage: string) => { - if (!errorMessage.toLowerCase().includes('not found') && - !errorMessage.toLowerCase().includes('agent run is not running')) { + if (!errorMessage.toLowerCase().includes('not found') && + !errorMessage.toLowerCase().includes('agent run is not running')) { toast.error(`Stream Error: ${errorMessage}`); } }, []); @@ -229,14 +230,15 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ try { const files = chatInputRef.current?.getPendingFiles() || []; - + const agentFormData = new FormData(); agentFormData.append('prompt', message); agentFormData.append('is_agent_builder', String(true)); agentFormData.append('target_agent_id', agentId); - + files.forEach((file) => { - agentFormData.append('files', file, file.name); + const normalizedName = normalizeFilenameToNFC(file.name); + agentFormData.append('files', file, normalizedName); }); if (options?.model_name) agentFormData.append('model_name', options.model_name); @@ -253,7 +255,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ console.log('[AGENT BUILDER] Setting agent run ID:', result.agent_run_id); setAgentRunId(result.agent_run_id); } - + const userMessage: UnifiedMessage = { message_id: `user-${Date.now()}`, thread_id: result.thread_id, @@ -336,8 +338,8 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ setAgentRunId(agentResult.agent_run_id); } catch (err) { toast.error(err instanceof Error ? err.message : 'Operation failed'); - setMessages((prev) => prev.map((m) => - m.message_id === optimisticUserMessage.message_id + setMessages((prev) => prev.map((m) => + m.message_id === optimisticUserMessage.message_id ? { ...m, message_id: `user-error-${Date.now()}` } : m )); @@ -363,7 +365,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ }, [stopStreaming, agentRunId, stopAgentMutation]); - const handleOpenFileViewer = useCallback(() => {}, []); + const handleOpenFileViewer = useCallback(() => { }, []); return ( @@ -375,7 +377,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({ streamingTextContent={streamingTextContent} streamingToolCall={streamingToolCall} agentStatus={agentStatus} - handleToolClick={() => {}} + handleToolClick={() => { }} handleOpenFileViewer={handleOpenFileViewer} streamHookStatus={streamHookStatus} agentName="Agent Builder" @@ -395,19 +397,19 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
+ ref={chatInputRef} + onSubmit={threadId ? handleSubmitMessage : handleSubmitFirstMessage} + loading={isSubmitting} + placeholder="Tell me how you'd like to configure your agent..." + value={inputValue} + onChange={setInputValue} + disabled={isSubmitting} + isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'} + onStopAgent={handleStopAgent} + agentName="Agent Builder" + hideAttachments={true} + bgColor='bg-muted-foreground/10' + />
); diff --git a/frontend/src/app/(dashboard)/agents/_components/agent-preview.tsx b/frontend/src/app/(dashboard)/agents/_components/agent-preview.tsx index 15c49619..c64d52e3 100644 --- a/frontend/src/app/(dashboard)/agents/_components/agent-preview.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/agent-preview.tsx @@ -3,9 +3,9 @@ import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; import { getAgentAvatar } from '../_utils/get-agent-style'; -import { - ChatInput, - ChatInputHandles +import { + ChatInput, + ChatInputHandles } from '@/components/thread/chat-input/chat-input'; import { ThreadContent } from '@/components/thread/content/ThreadContent'; import { UnifiedMessage } from '@/components/thread/types'; @@ -14,6 +14,7 @@ import { useAgentStream } from '@/hooks/useAgentStream'; import { useAddUserMessageMutation } from '@/hooks/react-query/threads/use-messages'; import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run'; import { BillingError } from '@/lib/api'; +import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; interface Agent { agent_id: string; @@ -39,7 +40,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => { const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'connecting' | 'error'>('idle'); const [isSubmitting, setIsSubmitting] = useState(false); const [hasStartedConversation, setHasStartedConversation] = useState(false); - + const messagesEndRef = useRef(null); const chatInputRef = useRef(null); @@ -71,7 +72,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => { const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => { console.log(`[PREVIEW STREAM] Received message: ID=${message.message_id}, Type=${message.type}`); - + setMessages((prev) => { const messageExists = prev.some((m) => m.message_id === message.message_id); if (messageExists) { @@ -105,8 +106,8 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => { const handleStreamError = useCallback((errorMessage: string) => { console.error(`[PREVIEW] Stream error: ${errorMessage}`); - if (!errorMessage.toLowerCase().includes('not found') && - !errorMessage.toLowerCase().includes('agent run is not running')) { + if (!errorMessage.toLowerCase().includes('not found') && + !errorMessage.toLowerCase().includes('agent run is not running')) { toast.error(`Stream Error: ${errorMessage}`); } }, []); @@ -176,13 +177,14 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => { try { const files = chatInputRef.current?.getPendingFiles() || []; - + const formData = new FormData(); formData.append('prompt', message); formData.append('agent_id', agent.agent_id); - + files.forEach((file, index) => { - formData.append('files', file, file.name); + const normalizedName = normalizeFilenameToNFC(file.name); + formData.append('files', file, normalizedName); }); if (options?.model_name) formData.append('model_name', options.model_name); @@ -328,7 +330,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => { return (
-
@@ -347,7 +349,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => { streamingToolCall={streamingToolCall} agentStatus={agentStatus} handleToolClick={handleToolClick} - handleOpenFileViewer={() => {}} + handleOpenFileViewer={() => { }} streamHookStatus={streamHookStatus} isPreviewMode={true} agentName={agent.name} diff --git a/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx b/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx index 6021f0a2..99669248 100644 --- a/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx +++ b/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx @@ -30,6 +30,7 @@ import { cn } from '@/lib/utils'; import { useModal } from '@/hooks/use-modal-store'; import { Examples } from './suggestions/examples'; import { useThreadQuery } from '@/hooks/react-query/threads/use-threads'; +import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; const PENDING_PROMPT_KEY = 'pendingAgentPrompt'; @@ -110,7 +111,8 @@ export function DashboardContent() { } files.forEach((file, index) => { - formData.append('files', file, file.name); + const normalizedName = normalizeFilenameToNFC(file.name); + formData.append('files', file, normalizedName); }); if (options?.model_name) formData.append('model_name', options.model_name); @@ -194,7 +196,7 @@ export function DashboardContent() {

Hey, I am

-
- +
- +
diff --git a/frontend/src/components/thread/chat-input/file-upload-handler.tsx b/frontend/src/components/thread/chat-input/file-upload-handler.tsx index 705dfb37..4485f231 100644 --- a/frontend/src/components/thread/chat-input/file-upload-handler.tsx +++ b/frontend/src/components/thread/chat-input/file-upload-handler.tsx @@ -14,6 +14,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { UploadedFile } from './chat-input'; +import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; @@ -32,17 +33,23 @@ const handleLocalFiles = ( setPendingFiles((prevFiles) => [...prevFiles, ...filteredFiles]); - const newUploadedFiles: UploadedFile[] = filteredFiles.map((file) => ({ - name: file.name, - path: `/workspace/${file.name}`, - size: file.size, - type: file.type || 'application/octet-stream', - localUrl: URL.createObjectURL(file) - })); + const newUploadedFiles: UploadedFile[] = filteredFiles.map((file) => { + // Normalize filename to NFC + const normalizedName = normalizeFilenameToNFC(file.name); + + return { + name: normalizedName, + path: `/workspace/${normalizedName}`, + size: file.size, + type: file.type || 'application/octet-stream', + localUrl: URL.createObjectURL(file) + }; + }); setUploadedFiles((prev) => [...prev, ...newUploadedFiles]); filteredFiles.forEach((file) => { - toast.success(`File attached: ${file.name}`); + const normalizedName = normalizeFilenameToNFC(file.name); + toast.success(`File attached: ${normalizedName}`); }); }; @@ -65,7 +72,9 @@ const uploadFiles = async ( continue; } - const uploadPath = `/workspace/${file.name}`; + // Normalize filename to NFC + const normalizedName = normalizeFilenameToNFC(file.name); + const uploadPath = `/workspace/${normalizedName}`; // Check if this filename already exists in chat messages const isFileInChat = messages.some(message => { @@ -74,7 +83,9 @@ const uploadFiles = async ( }); const formData = new FormData(); - formData.append('file', file); + // If the filename was normalized, append with the normalized name in the field name + // The server will use the path parameter for the actual filename + formData.append('file', file, normalizedName); formData.append('path', uploadPath); const supabase = createClient(); @@ -116,13 +127,13 @@ const uploadFiles = async ( } newUploadedFiles.push({ - name: file.name, + name: normalizedName, path: uploadPath, size: file.size, type: file.type || 'application/octet-stream', }); - toast.success(`File uploaded: ${file.name}`); + toast.success(`File uploaded: ${normalizedName}`); } setUploadedFiles((prev) => [...prev, ...newUploadedFiles]); diff --git a/frontend/src/components/thread/file-viewer-modal.tsx b/frontend/src/components/thread/file-viewer-modal.tsx index ad0379bd..dfc762fd 100644 --- a/frontend/src/components/thread/file-viewer-modal.tsx +++ b/frontend/src/components/thread/file-viewer-modal.tsx @@ -51,6 +51,7 @@ import { FileCache } from '@/hooks/react-query/files'; import JSZip from 'jszip'; +import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; // Define API_URL const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; @@ -1121,6 +1122,8 @@ export function FileViewerModal({ } }, []); + + // Process uploaded file - Define after helpers const processUpload = useCallback( async (event: React.ChangeEvent) => { @@ -1130,9 +1133,15 @@ export function FileViewerModal({ setIsUploading(true); try { + // Normalize filename to NFC + const normalizedName = normalizeFilenameToNFC(file.name); + const uploadPath = `${currentPath}/${normalizedName}`; + const formData = new FormData(); - formData.append('file', file); - formData.append('path', `${currentPath}/${file.name}`); + // If the filename was normalized, append with the normalized name in the field name + // The server will use the path parameter for the actual filename + formData.append('file', file, normalizedName); + formData.append('path', uploadPath); const supabase = createClient(); const { @@ -1162,7 +1171,7 @@ export function FileViewerModal({ // Reload the file list using React Query await refetchFiles(); - toast.success(`Uploaded: ${file.name}`); + toast.success(`Uploaded: ${normalizedName}`); } catch (error) { console.error('Upload failed:', error); toast.error( diff --git a/frontend/src/hooks/react-query/files/use-file-mutations.ts b/frontend/src/hooks/react-query/files/use-file-mutations.ts index 557ef5c9..c63f6c8f 100644 --- a/frontend/src/hooks/react-query/files/use-file-mutations.ts +++ b/frontend/src/hooks/react-query/files/use-file-mutations.ts @@ -3,7 +3,6 @@ import { useAuth } from '@/components/AuthProvider'; import { fileQueryKeys } from './use-file-queries'; import { FileCache } from '@/hooks/use-cached-file'; import { toast } from 'sonner'; - // Import the normalizePath function from use-file-queries function normalizePath(path: string): string { if (!path) return '/'; diff --git a/frontend/src/lib/utils/unicode.ts b/frontend/src/lib/utils/unicode.ts new file mode 100644 index 00000000..532a1710 --- /dev/null +++ b/frontend/src/lib/utils/unicode.ts @@ -0,0 +1,34 @@ +/** + * Normalize filename to NFC (Normalized Form Composed) to ensure consistent + * Unicode representation across different systems, especially macOS which + * can use NFD (Normalized Form Decomposed). + * + * @param filename The filename to normalize + * @returns The filename normalized to NFC form + */ +export const normalizeFilenameToNFC = (filename: string): string => { + try { + // Normalize to NFC (Normalized Form Composed) + return filename.normalize('NFC'); + } catch (error) { + console.warn('Failed to normalize filename to NFC:', filename, error); + return filename; + } +}; + +/** + * Normalize file path to NFC (Normalized Form Composed) to ensure consistent + * Unicode representation across different systems. + * + * @param path The file path to normalize + * @returns The path with all components normalized to NFC form + */ +export const normalizePathToNFC = (path: string): string => { + try { + // Normalize to NFC (Normalized Form Composed) + return path.normalize('NFC'); + } catch (error) { + console.warn('Failed to normalize path to NFC:', path, error); + return path; + } +}; \ No newline at end of file