diff --git a/app/actions.tsx b/app/actions.tsx index a2aa182d..19bf0844 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -128,9 +128,16 @@ async function submit(formData?: FormData, skip?: boolean) { isCollapsed: isCollapsed.value, }; } - const file = !skip ? (formData?.get('file') as File) : undefined + const files: File[] = [] + if (formData && !skip) { + for (const [key, value] of formData.entries()) { + if (key.startsWith('files[')) { + files.push(value as File) + } + } + } - if (!userInput && !file) { + if (!userInput && files.length === 0) { isGenerating.done(false) return { id: nanoid(), @@ -144,31 +151,31 @@ async function submit(formData?: FormData, skip?: boolean) { type: 'text' | 'image' text?: string image?: string - mimeType?: string }[] = [] if (userInput) { messageParts.push({ type: 'text', text: userInput }) } - if (file) { - const buffer = await file.arrayBuffer() - if (file.type.startsWith('image/')) { - const dataUrl = `data:${file.type};base64,${Buffer.from( - buffer - ).toString('base64')}` - messageParts.push({ - type: 'image', - image: dataUrl, - mimeType: file.type - }) - } else if (file.type === 'text/plain') { - const textContent = Buffer.from(buffer).toString('utf-8') - const existingTextPart = messageParts.find(p => p.type === 'text') - if (existingTextPart) { - existingTextPart.text = `${textContent}\n\n${existingTextPart.text}` - } else { - messageParts.push({ type: 'text', text: textContent }) + if (files.length > 0) { + for (const file of files) { + const buffer = await file.arrayBuffer() + if (file.type.startsWith('image/')) { + const dataUrl = `data:${file.type};base64,${Buffer.from( + buffer + ).toString('base64')}` + messageParts.push({ + type: 'image', + image: dataUrl + }) + } else if (file.type === 'text/plain') { + const textContent = Buffer.from(buffer).toString('utf-8') + const existingTextPart = messageParts.find(p => p.type === 'text') + if (existingTextPart) { + existingTextPart.text = `${textContent}\n\n${existingTextPart.text}` + } else { + messageParts.push({ type: 'text', text: textContent }) + } } } } @@ -180,7 +187,7 @@ async function submit(formData?: FormData, skip?: boolean) { const type = skip ? undefined - : formData?.has('input') || formData?.has('file') + : formData?.has('input') || files.length > 0 ? 'input' : formData?.has('related_query') ? 'input_related' diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index b0bf2166..9f87c273 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -3,13 +3,13 @@ import { useEffect, useState, useRef, ChangeEvent, forwardRef, useImperativeHandle } from 'react' import type { AI, UIState } from '@/app/actions' import { useUIState, useActions } from 'ai/rsc' -// Removed import of useGeospatialToolMcp as it's no longer used/available import { cn } from '@/lib/utils' import { UserMessage } from './user-message' import { Button } from './ui/button' import { ArrowRight, Plus, Paperclip, X } from 'lucide-react' import Textarea from 'react-textarea-autosize' import { nanoid } from 'nanoid' +import Image from 'next/image' interface ChatPanelProps { messages: UIState @@ -24,9 +24,8 @@ export interface ChatPanelRef { export const ChatPanel = forwardRef(({ messages, input, setInput }, ref) => { const [, setMessages] = useUIState() const { submit, clearChat } = useActions() - // Removed mcp instance as it's no longer passed to submit const [isMobile, setIsMobile] = useState(false) - const [selectedFile, setSelectedFile] = useState(null) + const [selectedFiles, setSelectedFiles] = useState([]) const inputRef = useRef(null) const formRef = useRef(null) const fileInputRef = useRef(null) @@ -48,22 +47,43 @@ export const ChatPanel = forwardRef(({ messages, i }, []) const handleFileChange = (e: ChangeEvent) => { - const file = e.target.files?.[0] - if (file) { - if (file.size > 10 * 1024 * 1024) { - alert('File size must be less than 10MB') - return - } - setSelectedFile(file) + const files = e.target.files + if (files) { + const newFiles = Array.from(files).filter(file => { + if (file.size > 10 * 1024 * 1024) { + alert(`File ${file.name} is too large (max 10MB)`) + return false + } + return true + }) + setSelectedFiles(prevFiles => [...prevFiles, ...newFiles]) } } + // Automatically submit the form when a file is selected on mobile + useEffect(() => { + if (selectedFiles.length > 0 && isMobile) { + // Only auto-submit if there wasn't text before, to avoid interrupting typing + if (input.trim() === '') { + formRef.current?.requestSubmit() + } + } + }, [selectedFiles, isMobile, input]) + const handleAttachmentClick = () => { fileInputRef.current?.click() } - const clearAttachment = () => { - setSelectedFile(null) + const removeAttachment = (index: number) => { + setSelectedFiles(prevFiles => prevFiles.filter((_, i) => i !== index)); + // Reset file input value to allow re-selecting the same file + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const clearAllAttachments = () => { + setSelectedFiles([]) if (fileInputRef.current) { fileInputRef.current.value = '' } @@ -71,7 +91,7 @@ export const ChatPanel = forwardRef(({ messages, i const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (!input && !selectedFile) { + if (!input && selectedFiles.length === 0) { return } @@ -79,12 +99,15 @@ export const ChatPanel = forwardRef(({ messages, i if (input) { content.push({ type: 'text', text: input }) } - if (selectedFile && selectedFile.type.startsWith('image/')) { - content.push({ - type: 'image', - image: URL.createObjectURL(selectedFile) - }) - } + selectedFiles.forEach(file => { + if (file.type.startsWith('image/')) { + content.push({ + type: 'image', + image: URL.createObjectURL(file) + }) + } + }) + setMessages(currentMessages => [ ...currentMessages, @@ -95,12 +118,14 @@ export const ChatPanel = forwardRef(({ messages, i ]) const formData = new FormData(e.currentTarget) - if (selectedFile) { - formData.append('file', selectedFile) + if (selectedFiles.length > 0) { + selectedFiles.forEach((file, index) => { + formData.append(`files[${index}]`, file) + }) } setInput('') - clearAttachment() + clearAllAttachments() const responseMessage = await submit(formData) setMessages(currentMessages => [...currentMessages, responseMessage as any]) @@ -108,7 +133,7 @@ export const ChatPanel = forwardRef(({ messages, i const handleClear = async () => { setMessages([]) - clearAttachment() + clearAllAttachments() await clearChat() } @@ -149,16 +174,32 @@ export const ChatPanel = forwardRef(({ messages, i : 'sticky bottom-0 bg-background z-10 w-full border-t border-border px-2 py-3 md:px-4' )} > - {selectedFile && ( + {selectedFiles.length > 0 && (
-
- - {selectedFile.name} - - -
+
+ {selectedFiles.map((file, index) => ( +
+ {file.name} + +
+ ))} +
)}
(({ messages, i ref={fileInputRef} onChange={handleFileChange} className="hidden" - accept="text/plain,image/png,image/jpeg,image/webp" + accept="image/png,image/jpeg,image/webp" + multiple /> {!isMobile && (