From 8ae549a06a7c5f879deeda7d913a6b9c8d9fc3b4 Mon Sep 17 00:00:00 2001 From: EreQ Date: Sun, 1 Feb 2026 10:22:30 +0300 Subject: [PATCH 1/8] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8a29bc5a..3d85d05f 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2024 Yoshiki Miura +Copyright 2025 Eric Ngoiya Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 3495da126a304f9283ea53cb33a628fdc9eb0f55 Mon Sep 17 00:00:00 2001 From: EreQ Date: Sun, 1 Feb 2026 10:23:13 +0300 Subject: [PATCH 2/8] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8a29bc5a..3d85d05f 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2024 Yoshiki Miura +Copyright 2025 Eric Ngoiya Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 6431b41d9870fda1e14d84f66b0bb7a9b770986b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:06:49 +0000 Subject: [PATCH 3/8] fix: reduce padding in mobile chat input area - Reduced vertical padding in `.mobile-chat-input-area` in `globals.css`. - Removed redundant `.mobile-chat-input` class and associated styles from `globals.css`. - Adjusted `ChatPanel` to use more compact padding and a smaller minimum height on mobile. - Removed excessive left padding (`pl-14`) on the mobile chat input since the attachment button is moved to the icons bar. - Cleaned up unused mobile-specific CSS classes in `globals.css`. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/globals.css | 57 +-------------------------------------- components/chat-panel.tsx | 11 ++++---- 2 files changed, 6 insertions(+), 62 deletions(-) diff --git a/app/globals.css b/app/globals.css index 22f95cba..5fd22aef 100644 --- a/app/globals.css +++ b/app/globals.css @@ -189,7 +189,7 @@ .mobile-chat-input-area { height: auto; - padding: 10px; + padding: 4px 10px; background-color: hsl(var(--background)); /* border-top: 1px solid hsl(var(--border)); */ /* Removed for cleaner separation */ border-bottom: 1px solid hsl(var(--border)); /* Added for separation from messages area below */ @@ -199,61 +199,6 @@ align-items: center; } - .mobile-chat-input { - /* position: relative; */ /* No longer fixed to bottom */ - /* bottom: 0; */ - /* left: 0; */ /* Handled by parent flex */ - /* right: 0; */ /* Handled by parent flex */ - width: 100%; /* Ensure it takes full width of its container */ - padding: 10px; - background-color: hsl(var(--background)); - /* border-top: 1px solid hsl(var(--border)); */ /* Removed to avoid double border */ - /* z-index: 30; */ /* No longer needed */ - } - - .mobile-chat-input input { - width: 100%; - padding: 8px; - border: 1px solid hsl(var(--input)); - border-radius: var(--radius); - background-color: hsl(var(--input)); - color: hsl(var(--foreground)); - box-sizing: border-box; - } - - .mobile-icons-bar-content .icon-button { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background-color: hsl(var(--secondary)); - color: hsl(var(--secondary-foreground)); - cursor: pointer; - } - - .mobile-icons-bar-content .icon-button:hover { - background-color: hsl(var(--secondary-foreground)); - color: hsl(var(--secondary)); - } - - .mobile-chat-input .icon-button { - position: absolute; - top: 50%; - transform: translateY(-50%); - background-color: transparent; - border: none; - cursor: pointer; - } - - .mobile-chat-input .icon-button.paperclip { - right: 40px; - } - - .mobile-chat-input .icon-button.arrow-right { - right: 10px; - } } /* Added for MapboxDraw controls */ diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index c45844d3..2a8c559e 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -199,13 +199,12 @@ export const ChatPanel = forwardRef(({ messages, i onSubmit={handleSubmit} className={cn( 'max-w-full w-full', - isMobile ? 'px-2 pb-2 pt-1 h-full flex flex-col justify-center' : '' + isMobile ? 'px-2 pb-1 pt-0 h-full flex flex-col justify-center' : '' )} >
@@ -241,10 +240,10 @@ export const ChatPanel = forwardRef(({ messages, i value={input} data-testid="chat-input" className={cn( - 'resize-none w-full min-h-12 rounded-fill border border-input pl-14 pr-12 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + 'resize-none w-full rounded-fill border border-input pr-12 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', isMobile - ? 'mobile-chat-input input bg-background' - : 'bg-muted' + ? 'bg-background min-h-10 pl-4 pt-2 pb-1' + : 'bg-muted min-h-12 pl-14 pt-3 pb-1' )} onChange={e => { setInput(e.target.value) From 10ab3fe3c8759b1424aff03b6536fd3931d644f8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:36:33 +0000 Subject: [PATCH 4/8] feat: update Stripe checkout links to new URL Replaced all occurrences of the old Stripe checkout link with the new URL: https://buy.stripe.com/14A3cv7K72TR3go14Nasg02 Updated files: - components/mobile-icons-bar.tsx - components/header.tsx Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/header.tsx | 4 ++-- components/mobile-icons-bar.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/header.tsx b/components/header.tsx index 644ba8c0..94a23801 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -53,7 +53,7 @@ export const Header = () => {
- + @@ -67,7 +67,7 @@ export const Header = () => { {/* Mobile menu buttons */}
- + diff --git a/components/mobile-icons-bar.tsx b/components/mobile-icons-bar.tsx index bde08487..d0db2cfa 100644 --- a/components/mobile-icons-bar.tsx +++ b/components/mobile-icons-bar.tsx @@ -48,7 +48,7 @@ export const MobileIconsBar: React.FC = ({ onAttachmentClic - + From 9a54a27129b8980155ad1d18bd0abb2de75db9e7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:30:45 +0000 Subject: [PATCH 5/8] feat: automatically convert large pasted text into file attachments This change implements a UX improvement where pasted text exceeding 5,000 characters is automatically converted into a `.txt` file attachment named `pasted-text.txt`. Key changes: - Added `onPaste` handler to the chat input `Textarea`. - Implemented character threshold (5,000) for auto-conversion. - Added collision handling: if a file is already attached, the large paste is prevented and a toast notification is shown. - Updated file size validation to use `sonner` toasts instead of browser alerts for consistency. - Ensured `e.preventDefault()` is called for large pastes to prevent UI clogging. - Verified functionality with targeted Playwright tests and frontend screenshots. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/chat-panel.tsx | 22 +++++++++++++++++++++- server.log | 11 ----------- 2 files changed, 21 insertions(+), 12 deletions(-) delete mode 100644 server.log diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 2a8c559e..fd600cc9 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,31 @@ 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 > 5000) { + e.preventDefault() + 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 +268,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/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 From d5d090d460eb7a7a92006c7d2f3e41800f8e7db6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:36:15 +0000 Subject: [PATCH 6/8] feat: final implementation of large paste conversion with collision handling and input clearing This commit ensures: - Large pastes (>5000 chars) are converted to a file attachment. - Chat input is cleared upon conversion to allow for follow-up text. - Paste is prevented (preventDefault) even on collision to keep UI clean. - All validation uses sonner toasts for consistent UI. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> From 5b0855e46f6abdd2a81f1736dfd54af139bd755f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:50:58 +0000 Subject: [PATCH 7/8] feat: lower large paste threshold and add 10MB limit check - Lowered the threshold for automatic file conversion from 5,000 to 1,000 characters to better catch long inputs. - Added a 10MB limit check for pasted content to ensure consistency with file upload limits. - Ensured input is cleared and default behavior is prevented on all large pastes. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/chat-panel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index fd600cc9..481bbc2f 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -79,8 +79,12 @@ export const ChatPanel = forwardRef(({ messages, i const handlePaste = (e: React.ClipboardEvent) => { const pastedText = e.clipboardData.getData('text') - if (pastedText.length > 5000) { + if (pastedText.length > 1000) { 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' From 88731f36097766ce4a1be2a491f382e397a19aa3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:17:44 +0000 Subject: [PATCH 8/8] feat: add automatic conversion of large pasted text to file attachments - Intercept paste events in `ChatPanel` and convert text > 500 chars to `pasted-text.txt`. - Clear the textarea immediately upon conversion. - Unify toast notifications by switching from `react-toastify` to `sonner` in `ChatPanel` and `HeaderSearchButton`. - Add E2E tests for paste-to-file conversion logic. - Fix regression in Resolution Search by ensuring consistent notification library usage. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- components/chat-panel.tsx | 2 +- components/header-search-button.tsx | 2 +- tests/paste.spec.ts | 82 +++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 tests/paste.spec.ts diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 481bbc2f..42da9290 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -79,7 +79,7 @@ export const ChatPanel = forwardRef(({ messages, i const handlePaste = (e: React.ClipboardEvent) => { const pastedText = e.clipboardData.getData('text') - if (pastedText.length > 1000) { + if (pastedText.length > 500) { e.preventDefault() if (pastedText.length > 10 * 1024 * 1024) { toast.error('Pasted text exceeds 10MB limit') 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/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(); + }); +});