const handleSubmit = useCallback( async (message: PromptInputMessage) => { const hasText = Boolean(message.text); const hasAttachments = Boolean(message.files?.length); if (!(hasText || hasAttachments)) { return; } let currentThreadId = threadId; // Create thread if it doesn't exist if (!currentThreadId) { const result = await createThreadAction("New Chat"); if (result.success) { currentThreadId = result.thread.id; // Navigate to the newly created thread route (preserve locale) if (locale) { router.push(`/${locale}/agent/video/${currentThreadId}`); } else { router.push(`/agent/video/${currentThreadId}`); } } else { console.error("Failed to create thread:", result.error); // TODO: Show user-friendly error message return; } } // Upload files to Vercel Blob if they have data: URLs (base64) let processedFiles = message.files; if (message.files && message.files.length > 0) { setIsUploadingFiles(true); try { processedFiles = await Promise.all( message.files.map(async (file) => { // Check if this is a data URL that needs uploading if (file.url?.startsWith("data:")) { // Convert data URL to Blob const response = await fetch(file.url); const blob = await response.blob(); // Create a File object from the blob const fileObj = new File([blob], file.filename || "upload", { type: file.mediaType || blob.type, }); // Upload to Vercel Blob const uploadUrl = `/api/upload?filename=${encodeURIComponent(fileObj.name)}`; const uploadResponse = await fetch(uploadUrl, { method: "POST", body: fileObj, headers: { "Content-Type": fileObj.type, }, }); if (!uploadResponse.ok) { throw new Error("Failed to upload file"); } const uploadedBlob = await uploadResponse.json(); // Return the file with the Vercel Blob URL return { ...file, url: uploadedBlob.url, }; } // If not a data URL, return as-is (already uploaded) return file; }), ); } catch (error) { console.error("File upload failed:", error); // TODO: Show user-friendly error message return; } finally { setIsUploadingFiles(false); } } const convertedText = message.text ? convertToMarkdown(message.text) : "Sent with attachments"; sendMessage( { text: convertedText, files: processedFiles, }, { body: { threadId: currentThreadId, model: selectedModel, duration, withAudio, }, }, ); setInput(""); // Reset the prompt input to clear attachments setPromptInputKey((prev) => prev + 1); // Generate title for the first message in the thread if (!hasTitleGenerated && message.text && messages.length === 0) { setHasTitleGenerated(true); // Generate title asynchronously without blocking the UI generateThreadTitleAction(currentThreadId, message.text) .then((result) => { if (result.success) { console.log("Thread title generated:", result.title); // Invalidate queries to refresh the sidebar with new title queryClient.invalidateQueries({ queryKey: ["threads"] }); queryClient.invalidateQueries({ queryKey: ["thread", currentThreadId] }); } else { console.error("Failed to generate thread title:", result.error); } }) .catch((error) => { console.error("Error generating thread title:", error); }); } }, [ threadId, router, locale, sendMessage, messages.length, hasTitleGenerated, selectedModel, duration, withAudio, ], );