mirror of https://github.com/kortix-ai/suna.git
Merge pull request #719 from kubet/fix/filename-conversion
fix: filename conversion
This commit is contained in:
commit
e93bdab2dd
|
@ -18,6 +18,7 @@ import { Check, Clock } from 'lucide-react';
|
||||||
import { BillingError } from '@/lib/api';
|
import { BillingError } from '@/lib/api';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { agentKeys } from '@/hooks/react-query/agents/keys';
|
import { agentKeys } from '@/hooks/react-query/agents/keys';
|
||||||
|
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||||
|
|
||||||
interface AgentBuilderChatProps {
|
interface AgentBuilderChatProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
@ -27,8 +28,8 @@ interface AgentBuilderChatProps {
|
||||||
currentStyle: { avatar: string; color: string };
|
currentStyle: { avatar: string; color: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
agentId,
|
agentId,
|
||||||
formData,
|
formData,
|
||||||
handleFieldChange,
|
handleFieldChange,
|
||||||
handleStyleChange,
|
handleStyleChange,
|
||||||
|
@ -42,7 +43,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const chatInputRef = useRef<ChatInputHandles>(null);
|
const chatInputRef = useRef<ChatInputHandles>(null);
|
||||||
const previousMessageCountRef = useRef(0);
|
const previousMessageCountRef = useRef(0);
|
||||||
|
@ -114,7 +115,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
setThreadId(thread_id);
|
setThreadId(thread_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasInitiallyLoadedRef.current = true;
|
hasInitiallyLoadedRef.current = true;
|
||||||
} else if (chatHistoryQuery.status === 'error') {
|
} else if (chatHistoryQuery.status === 'error') {
|
||||||
console.error('[AgentBuilderChat] Error loading chat history:', chatHistoryQuery.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) => {
|
const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => {
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
if (!prev) prev = [];
|
if (!prev) prev = [];
|
||||||
|
|
||||||
if (message.type === 'user') {
|
if (message.type === 'user') {
|
||||||
const optimisticIndex = prev.findIndex(m =>
|
const optimisticIndex = prev.findIndex(m =>
|
||||||
m.message_id.startsWith('temp-user-') &&
|
m.message_id.startsWith('temp-user-') &&
|
||||||
m.content === message.content &&
|
m.content === message.content &&
|
||||||
m.type === 'user'
|
m.type === 'user'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (optimisticIndex !== -1) {
|
if (optimisticIndex !== -1) {
|
||||||
console.log(`[AGENT BUILDER] Replacing optimistic message with real message`);
|
console.log(`[AGENT BUILDER] Replacing optimistic message with real message`);
|
||||||
const newMessages = [...prev];
|
const newMessages = [...prev];
|
||||||
|
@ -176,8 +177,8 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStreamError = useCallback((errorMessage: string) => {
|
const handleStreamError = useCallback((errorMessage: string) => {
|
||||||
if (!errorMessage.toLowerCase().includes('not found') &&
|
if (!errorMessage.toLowerCase().includes('not found') &&
|
||||||
!errorMessage.toLowerCase().includes('agent run is not running')) {
|
!errorMessage.toLowerCase().includes('agent run is not running')) {
|
||||||
toast.error(`Stream Error: ${errorMessage}`);
|
toast.error(`Stream Error: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -229,14 +230,15 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = chatInputRef.current?.getPendingFiles() || [];
|
const files = chatInputRef.current?.getPendingFiles() || [];
|
||||||
|
|
||||||
const agentFormData = new FormData();
|
const agentFormData = new FormData();
|
||||||
agentFormData.append('prompt', message);
|
agentFormData.append('prompt', message);
|
||||||
agentFormData.append('is_agent_builder', String(true));
|
agentFormData.append('is_agent_builder', String(true));
|
||||||
agentFormData.append('target_agent_id', agentId);
|
agentFormData.append('target_agent_id', agentId);
|
||||||
|
|
||||||
files.forEach((file) => {
|
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);
|
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);
|
console.log('[AGENT BUILDER] Setting agent run ID:', result.agent_run_id);
|
||||||
setAgentRunId(result.agent_run_id);
|
setAgentRunId(result.agent_run_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage: UnifiedMessage = {
|
const userMessage: UnifiedMessage = {
|
||||||
message_id: `user-${Date.now()}`,
|
message_id: `user-${Date.now()}`,
|
||||||
thread_id: result.thread_id,
|
thread_id: result.thread_id,
|
||||||
|
@ -336,8 +338,8 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
setAgentRunId(agentResult.agent_run_id);
|
setAgentRunId(agentResult.agent_run_id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : 'Operation failed');
|
toast.error(err instanceof Error ? err.message : 'Operation failed');
|
||||||
setMessages((prev) => prev.map((m) =>
|
setMessages((prev) => prev.map((m) =>
|
||||||
m.message_id === optimisticUserMessage.message_id
|
m.message_id === optimisticUserMessage.message_id
|
||||||
? { ...m, message_id: `user-error-${Date.now()}` }
|
? { ...m, message_id: `user-error-${Date.now()}` }
|
||||||
: m
|
: m
|
||||||
));
|
));
|
||||||
|
@ -363,7 +365,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
}, [stopStreaming, agentRunId, stopAgentMutation]);
|
}, [stopStreaming, agentRunId, stopAgentMutation]);
|
||||||
|
|
||||||
|
|
||||||
const handleOpenFileViewer = useCallback(() => {}, []);
|
const handleOpenFileViewer = useCallback(() => { }, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -375,7 +377,7 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
streamingTextContent={streamingTextContent}
|
streamingTextContent={streamingTextContent}
|
||||||
streamingToolCall={streamingToolCall}
|
streamingToolCall={streamingToolCall}
|
||||||
agentStatus={agentStatus}
|
agentStatus={agentStatus}
|
||||||
handleToolClick={() => {}}
|
handleToolClick={() => { }}
|
||||||
handleOpenFileViewer={handleOpenFileViewer}
|
handleOpenFileViewer={handleOpenFileViewer}
|
||||||
streamHookStatus={streamHookStatus}
|
streamHookStatus={streamHookStatus}
|
||||||
agentName="Agent Builder"
|
agentName="Agent Builder"
|
||||||
|
@ -395,19 +397,19 @@ export const AgentBuilderChat = React.memo(function AgentBuilderChat({
|
||||||
|
|
||||||
<div className="flex-shrink-0 md:pb-4 md:px-12 px-4">
|
<div className="flex-shrink-0 md:pb-4 md:px-12 px-4">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
ref={chatInputRef}
|
ref={chatInputRef}
|
||||||
onSubmit={threadId ? handleSubmitMessage : handleSubmitFirstMessage}
|
onSubmit={threadId ? handleSubmitMessage : handleSubmitFirstMessage}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
placeholder="Tell me how you'd like to configure your agent..."
|
placeholder="Tell me how you'd like to configure your agent..."
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={setInputValue}
|
onChange={setInputValue}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
|
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
|
||||||
onStopAgent={handleStopAgent}
|
onStopAgent={handleStopAgent}
|
||||||
agentName="Agent Builder"
|
agentName="Agent Builder"
|
||||||
hideAttachments={true}
|
hideAttachments={true}
|
||||||
bgColor='bg-muted-foreground/10'
|
bgColor='bg-muted-foreground/10'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Badge } from '@/components/ui/badge';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getAgentAvatar } from '../_utils/get-agent-style';
|
import { getAgentAvatar } from '../_utils/get-agent-style';
|
||||||
import {
|
import {
|
||||||
ChatInput,
|
ChatInput,
|
||||||
ChatInputHandles
|
ChatInputHandles
|
||||||
} from '@/components/thread/chat-input/chat-input';
|
} from '@/components/thread/chat-input/chat-input';
|
||||||
import { ThreadContent } from '@/components/thread/content/ThreadContent';
|
import { ThreadContent } from '@/components/thread/content/ThreadContent';
|
||||||
import { UnifiedMessage } from '@/components/thread/types';
|
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 { useAddUserMessageMutation } from '@/hooks/react-query/threads/use-messages';
|
||||||
import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run';
|
import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run';
|
||||||
import { BillingError } from '@/lib/api';
|
import { BillingError } from '@/lib/api';
|
||||||
|
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||||
|
|
||||||
interface Agent {
|
interface Agent {
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
|
@ -39,7 +40,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||||
const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'connecting' | 'error'>('idle');
|
const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'connecting' | 'error'>('idle');
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const chatInputRef = useRef<ChatInputHandles>(null);
|
const chatInputRef = useRef<ChatInputHandles>(null);
|
||||||
|
|
||||||
|
@ -71,7 +72,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||||
|
|
||||||
const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => {
|
const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => {
|
||||||
console.log(`[PREVIEW STREAM] Received message: ID=${message.message_id}, Type=${message.type}`);
|
console.log(`[PREVIEW STREAM] Received message: ID=${message.message_id}, Type=${message.type}`);
|
||||||
|
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const messageExists = prev.some((m) => m.message_id === message.message_id);
|
const messageExists = prev.some((m) => m.message_id === message.message_id);
|
||||||
if (messageExists) {
|
if (messageExists) {
|
||||||
|
@ -105,8 +106,8 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||||
|
|
||||||
const handleStreamError = useCallback((errorMessage: string) => {
|
const handleStreamError = useCallback((errorMessage: string) => {
|
||||||
console.error(`[PREVIEW] Stream error: ${errorMessage}`);
|
console.error(`[PREVIEW] Stream error: ${errorMessage}`);
|
||||||
if (!errorMessage.toLowerCase().includes('not found') &&
|
if (!errorMessage.toLowerCase().includes('not found') &&
|
||||||
!errorMessage.toLowerCase().includes('agent run is not running')) {
|
!errorMessage.toLowerCase().includes('agent run is not running')) {
|
||||||
toast.error(`Stream Error: ${errorMessage}`);
|
toast.error(`Stream Error: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -176,13 +177,14 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = chatInputRef.current?.getPendingFiles() || [];
|
const files = chatInputRef.current?.getPendingFiles() || [];
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('prompt', message);
|
formData.append('prompt', message);
|
||||||
formData.append('agent_id', agent.agent_id);
|
formData.append('agent_id', agent.agent_id);
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
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);
|
if (options?.model_name) formData.append('model_name', options.model_name);
|
||||||
|
@ -328,7 +330,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-muted dark:bg-muted/30">
|
<div className="h-full flex flex-col bg-muted dark:bg-muted/30">
|
||||||
<div className="flex-shrink-0 flex items-center gap-3 p-8">
|
<div className="flex-shrink-0 flex items-center gap-3 p-8">
|
||||||
<div
|
<div
|
||||||
className="h-10 w-10 flex items-center justify-center rounded-lg text-lg"
|
className="h-10 w-10 flex items-center justify-center rounded-lg text-lg"
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color }}
|
||||||
>
|
>
|
||||||
|
@ -347,7 +349,7 @@ export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||||
streamingToolCall={streamingToolCall}
|
streamingToolCall={streamingToolCall}
|
||||||
agentStatus={agentStatus}
|
agentStatus={agentStatus}
|
||||||
handleToolClick={handleToolClick}
|
handleToolClick={handleToolClick}
|
||||||
handleOpenFileViewer={() => {}}
|
handleOpenFileViewer={() => { }}
|
||||||
streamHookStatus={streamHookStatus}
|
streamHookStatus={streamHookStatus}
|
||||||
isPreviewMode={true}
|
isPreviewMode={true}
|
||||||
agentName={agent.name}
|
agentName={agent.name}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { cn } from '@/lib/utils';
|
||||||
import { useModal } from '@/hooks/use-modal-store';
|
import { useModal } from '@/hooks/use-modal-store';
|
||||||
import { Examples } from './suggestions/examples';
|
import { Examples } from './suggestions/examples';
|
||||||
import { useThreadQuery } from '@/hooks/react-query/threads/use-threads';
|
import { useThreadQuery } from '@/hooks/react-query/threads/use-threads';
|
||||||
|
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||||
|
|
||||||
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
|
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
|
||||||
|
|
||||||
|
@ -110,7 +111,8 @@ export function DashboardContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
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);
|
if (options?.model_name) formData.append('model_name', options.model_name);
|
||||||
|
@ -194,7 +196,7 @@ export function DashboardContent() {
|
||||||
<h1 className="tracking-tight text-4xl text-muted-foreground leading-tight">
|
<h1 className="tracking-tight text-4xl text-muted-foreground leading-tight">
|
||||||
Hey, I am
|
Hey, I am
|
||||||
</h1>
|
</h1>
|
||||||
<AgentSelector
|
<AgentSelector
|
||||||
selectedAgentId={selectedAgentId}
|
selectedAgentId={selectedAgentId}
|
||||||
onAgentSelect={setSelectedAgentId}
|
onAgentSelect={setSelectedAgentId}
|
||||||
variant="heading"
|
variant="heading"
|
||||||
|
@ -204,7 +206,7 @@ export function DashboardContent() {
|
||||||
What would you like to do today?
|
What would you like to do today?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-full mb-2",
|
"w-full mb-2",
|
||||||
"max-w-full",
|
"max-w-full",
|
||||||
|
@ -220,7 +222,7 @@ export function DashboardContent() {
|
||||||
hideAttachments={false}
|
hideAttachments={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Examples onSelectPrompt={setInputValue} />
|
<Examples onSelectPrompt={setInputValue} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import { UploadedFile } from './chat-input';
|
import { UploadedFile } from './chat-input';
|
||||||
|
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||||
|
|
||||||
|
@ -32,17 +33,23 @@ const handleLocalFiles = (
|
||||||
|
|
||||||
setPendingFiles((prevFiles) => [...prevFiles, ...filteredFiles]);
|
setPendingFiles((prevFiles) => [...prevFiles, ...filteredFiles]);
|
||||||
|
|
||||||
const newUploadedFiles: UploadedFile[] = filteredFiles.map((file) => ({
|
const newUploadedFiles: UploadedFile[] = filteredFiles.map((file) => {
|
||||||
name: file.name,
|
// Normalize filename to NFC
|
||||||
path: `/workspace/${file.name}`,
|
const normalizedName = normalizeFilenameToNFC(file.name);
|
||||||
size: file.size,
|
|
||||||
type: file.type || 'application/octet-stream',
|
return {
|
||||||
localUrl: URL.createObjectURL(file)
|
name: normalizedName,
|
||||||
}));
|
path: `/workspace/${normalizedName}`,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type || 'application/octet-stream',
|
||||||
|
localUrl: URL.createObjectURL(file)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
setUploadedFiles((prev) => [...prev, ...newUploadedFiles]);
|
setUploadedFiles((prev) => [...prev, ...newUploadedFiles]);
|
||||||
filteredFiles.forEach((file) => {
|
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;
|
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
|
// Check if this filename already exists in chat messages
|
||||||
const isFileInChat = messages.some(message => {
|
const isFileInChat = messages.some(message => {
|
||||||
|
@ -74,7 +83,9 @@ const uploadFiles = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = new FormData();
|
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);
|
formData.append('path', uploadPath);
|
||||||
|
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
|
@ -116,13 +127,13 @@ const uploadFiles = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
newUploadedFiles.push({
|
newUploadedFiles.push({
|
||||||
name: file.name,
|
name: normalizedName,
|
||||||
path: uploadPath,
|
path: uploadPath,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: file.type || 'application/octet-stream',
|
type: file.type || 'application/octet-stream',
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success(`File uploaded: ${file.name}`);
|
toast.success(`File uploaded: ${normalizedName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadedFiles((prev) => [...prev, ...newUploadedFiles]);
|
setUploadedFiles((prev) => [...prev, ...newUploadedFiles]);
|
||||||
|
|
|
@ -51,6 +51,7 @@ import {
|
||||||
FileCache
|
FileCache
|
||||||
} from '@/hooks/react-query/files';
|
} from '@/hooks/react-query/files';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||||
|
|
||||||
// Define API_URL
|
// Define API_URL
|
||||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||||
|
@ -1121,6 +1122,8 @@ export function FileViewerModal({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Process uploaded file - Define after helpers
|
// Process uploaded file - Define after helpers
|
||||||
const processUpload = useCallback(
|
const processUpload = useCallback(
|
||||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
@ -1130,9 +1133,15 @@ export function FileViewerModal({
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Normalize filename to NFC
|
||||||
|
const normalizedName = normalizeFilenameToNFC(file.name);
|
||||||
|
const uploadPath = `${currentPath}/${normalizedName}`;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
// If the filename was normalized, append with the normalized name in the field name
|
||||||
formData.append('path', `${currentPath}/${file.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 supabase = createClient();
|
||||||
const {
|
const {
|
||||||
|
@ -1162,7 +1171,7 @@ export function FileViewerModal({
|
||||||
// Reload the file list using React Query
|
// Reload the file list using React Query
|
||||||
await refetchFiles();
|
await refetchFiles();
|
||||||
|
|
||||||
toast.success(`Uploaded: ${file.name}`);
|
toast.success(`Uploaded: ${normalizedName}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload failed:', error);
|
console.error('Upload failed:', error);
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useAuth } from '@/components/AuthProvider';
|
||||||
import { fileQueryKeys } from './use-file-queries';
|
import { fileQueryKeys } from './use-file-queries';
|
||||||
import { FileCache } from '@/hooks/use-cached-file';
|
import { FileCache } from '@/hooks/use-cached-file';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
// Import the normalizePath function from use-file-queries
|
// Import the normalizePath function from use-file-queries
|
||||||
function normalizePath(path: string): string {
|
function normalizePath(path: string): string {
|
||||||
if (!path) return '/';
|
if (!path) return '/';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue