diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 2a8c559e..42da9290 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useRef, ChangeEvent, forwardRef, useImperativeHandle, useCallback } from 'react' import type { AI, UIState } from '@/app/actions' import { useUIState, useActions, readStreamableValue } from 'ai/rsc' +import { toast } from 'sonner' // Removed import of useGeospatialToolMcp as it's no longer used/available import { cn } from '@/lib/utils' import { UserMessage } from './user-message' @@ -69,13 +70,35 @@ export const ChatPanel = forwardRef(({ messages, i const file = e.target.files?.[0] if (file) { if (file.size > 10 * 1024 * 1024) { - alert('File size must be less than 10MB') + toast.error('File size must be less than 10MB') return } setSelectedFile(file) } } + const handlePaste = (e: React.ClipboardEvent) => { + const pastedText = e.clipboardData.getData('text') + if (pastedText.length > 500) { + e.preventDefault() + if (pastedText.length > 10 * 1024 * 1024) { + toast.error('Pasted text exceeds 10MB limit') + return + } + if (selectedFile) { + toast.error( + 'Please remove the current attachment to convert large paste to file' + ) + return + } + const file = new File([pastedText], 'pasted-text.txt', { + type: 'text/plain' + }) + setSelectedFile(file) + setInput('') + } + } + const handleAttachmentClick = () => { fileInputRef.current?.click() } @@ -249,6 +272,7 @@ export const ChatPanel = forwardRef(({ messages, i setInput(e.target.value) debouncedGetSuggestions(e.target.value) }} + onPaste={handlePaste} onKeyDown={e => { if ( e.key === 'Enter' && diff --git a/components/header-search-button.tsx b/components/header-search-button.tsx index 92cb1c65..0feccdb5 100644 --- a/components/header-search-button.tsx +++ b/components/header-search-button.tsx @@ -9,7 +9,7 @@ import { useActions, useUIState } from 'ai/rsc' import { AI } from '@/app/actions' import { nanoid } from 'nanoid' import { UserMessage } from './user-message' -import { toast } from 'react-toastify' +import { toast } from 'sonner' import { useSettingsStore } from '@/lib/store/settings' import { useMapData } from './map/map-data-context' diff --git a/server.log b/server.log deleted file mode 100644 index 45044476..00000000 --- a/server.log +++ /dev/null @@ -1,11 +0,0 @@ -$ next dev --turbo - ⚠ Port 3000 is in use, using available port 3003 instead. - ▲ Next.js 15.3.6 (Turbopack) - - Local: http://localhost:3003 - - Network: http://192.168.0.2:3003 - - Environments: .env.local, .env - - ✓ Starting... - ○ Compiling middleware ... - ✓ Compiled middleware in 648ms - ✓ Ready in 2.5s diff --git a/tests/paste.spec.ts b/tests/paste.spec.ts new file mode 100644 index 00000000..935b16c5 --- /dev/null +++ b/tests/paste.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Paste to File Conversion', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('converts large pasted text to file', async ({ page }) => { + const chatInput = page.getByTestId('chat-input'); + + // Create a large text string (> 500 chars) + const largeText = 'A'.repeat(501); + + // Simulate paste + await chatInput.focus(); + await page.evaluate((text) => { + const dt = new DataTransfer(); + dt.setData('text/plain', text); + const event = new ClipboardEvent('paste', { + clipboardData: dt, + bubbles: true, + cancelable: true + }); + document.activeElement?.dispatchEvent(event); + }, largeText); + + // Check if attachment exists + const attachment = page.getByText('pasted-text.txt'); + await expect(attachment).toBeVisible(); + + // Check if input is empty + await expect(chatInput).toHaveValue(''); + }); + + test('does not convert small pasted text', async ({ page }) => { + const chatInput = page.getByTestId('chat-input'); + + const smallText = 'Small snippet'; + + // Simulate paste + await chatInput.focus(); + await page.evaluate((text) => { + const dt = new DataTransfer(); + dt.setData('text/plain', text); + const event = new ClipboardEvent('paste', { + clipboardData: dt, + bubbles: true, + cancelable: true + }); + document.activeElement?.dispatchEvent(event); + }, smallText); + + // Check that attachment does NOT exist + const attachment = page.getByText('pasted-text.txt'); + await expect(attachment).not.toBeVisible(); + }); + + test('shows error when pasting while file already attached', async ({ page }) => { + const chatInput = page.getByTestId('chat-input'); + + const largeText1 = 'A'.repeat(501); + await chatInput.focus(); + await page.evaluate((text) => { + const dt = new DataTransfer(); + dt.setData('text/plain', text); + const event = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }); + document.activeElement?.dispatchEvent(event); + }, largeText1); + + await expect(page.getByText('pasted-text.txt')).toBeVisible(); + + const largeText2 = 'B'.repeat(501); + await page.evaluate((text) => { + const dt = new DataTransfer(); + dt.setData('text/plain', text); + const event = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }); + document.activeElement?.dispatchEvent(event); + }, largeText2); + + await expect(page.getByText('Please remove the current attachment')).toBeVisible(); + }); +});