Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 1 addition & 56 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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 */
Expand Down
33 changes: 26 additions & 7 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -69,13 +70,31 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ 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('')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

setInput('') clears any pre-typed text.

If a user types some context before pasting large text, this call erases their typed content. Per PR discussion, e.preventDefault() already stops the pasted text from being inserted, so clearing input may not be necessary.

Consider preserving existing input or showing a success toast so users understand why their input changed.

♻️ Option A: Remove setInput to preserve typed text
       setSelectedFile(file)
-      setInput('')
+      toast.success('Large text converted to file attachment')
♻️ Option B: Add ref guard to prevent race condition (per PR discussion)

If you need to clear input to prevent race conditions with onChange, use a ref to guard against the change event:

+  const isConvertingPaste = useRef(false)
+
   const handlePaste = (e: React.ClipboardEvent) => {
     const pastedText = e.clipboardData.getData('text')
     if (pastedText.length > 1000) {
       e.preventDefault()
       // ... size and attachment checks ...
+      isConvertingPaste.current = true
       const file = new File([pastedText], 'pasted-text.txt', {
         type: 'text/plain'
       })
       setSelectedFile(file)
       setInput('')
+      toast.success('Large text converted to file attachment')
+      // Reset flag after state updates
+      setTimeout(() => { isConvertingPaste.current = false }, 0)
     }
   }

Then guard the onChange:

   onChange={e => {
+    if (isConvertingPaste.current) return
     setInput(e.target.value)
     debouncedGetSuggestions(e.target.value)
   }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setInput('')
setSelectedFile(file)
toast.success('Large text converted to file attachment')
🤖 Prompt for AI Agents
In `@components/chat-panel.tsx` at line 98, The call to setInput('') in the paste
handler is clearing any pre-typed user text; remove that setInput('') so the
existing input is preserved (locate the onPaste / paste handler in
components/chat-panel.tsx and delete the setInput('') statement), or if you must
clear to avoid a race with onChange implement the ref-based guard discussed in
the PR: add a boolean ref (e.g., skipOnChangeRef) and set it around the paste
flow, then check skipOnChangeRef.current inside the onChange handler to ignore
the spurious update instead of wiping the input.

}
}
Comment on lines 80 to 100
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The paste threshold (5000) is a magic number here. It should be extracted into a named constant (ideally colocated with related upload/validation constants) so it’s easier to discover, adjust, and test consistently across the codebase.

Suggestion

Extract the threshold into a constant (and consider reusing it in tests).

const LARGE_PASTE_CHAR_THRESHOLD = 5000

const handlePaste = (e: React.ClipboardEvent) => {
  const pastedText = e.clipboardData.getData('text')
  if (pastedText.length > LARGE_PASTE_CHAR_THRESHOLD) {
    // ...
  }
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 80 to 100
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handlePaste always reads text from the clipboard. In some browsers/contexts, a paste may primarily contain files/HTML, and forcing getData('text') + large-text prevention could accidentally interfere with non-text pastes. Consider guarding so this logic only runs when there is meaningful plain-text content and otherwise lets the default paste behavior proceed.

Suggestion

Add conservative guards so you only intercept large plain-text pastes and do not block other clipboard payloads.

const handlePaste = (e: React.ClipboardEvent) => {
  const text = e.clipboardData.getData('text/plain')
  if (!text) return

  if (text.length > LARGE_PASTE_CHAR_THRESHOLD) {
    e.preventDefault()
    if (selectedFile) {
      toast.error('Please remove the current attachment to convert large paste to file')
      return
    }

    setSelectedFile(new File([text], 'pasted-text.txt', { type: 'text/plain' }))
    setInput('')
  }
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 80 to 100
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting pasted text into a File bypasses the existing 10MB check in handleFileChange. A user could paste extremely large text and end up with a huge in-memory File (and later potentially a failed upload/server rejection). You should apply the same size limit (and ideally share the constant) when creating the file from pasted content.

Suggestion

Reuse the same max-size validation for pasted-text files before calling setSelectedFile.

const MAX_FILE_BYTES = 10 * 1024 * 1024

// ... inside large paste branch
const file = new File([text], 'pasted-text.txt', { type: 'text/plain' })
if (file.size > MAX_FILE_BYTES) {
  toast.error('File size must be less than 10MB')
  return
}
setSelectedFile(file)
setInput('')

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 80 to 100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding a success toast when paste is converted to file.

When large text is converted to a file, the user sees their input cleared and a file attachment appear. Adding a success toast would make this behavior more discoverable and less confusing.

Additionally, consider wrapping handlePaste in useCallback for consistency with debouncedGetSuggestions and to avoid unnecessary re-renders if the Textarea memoizes props.

♻️ Proposed enhancement
-  const handlePaste = (e: React.ClipboardEvent) => {
+  const handlePaste = useCallback((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('')
+      toast.success('Large text converted to file attachment')
     }
-  }
+  }, [selectedFile, setInput])
🤖 Prompt for AI Agents
In `@components/chat-panel.tsx` around lines 80 - 96, Add a success toast when
large pasted text is converted to a File: inside handlePaste, after creating the
File and calling setSelectedFile(file) and setInput(''), call toast.success with
a brief message like "Pasted text attached as file" so the user sees the
conversion. Also wrap handlePaste in React.useCallback (same pattern as
debouncedGetSuggestions) and include its dependencies (selectedFile,
setSelectedFile, setInput, toast) to avoid unnecessary re-renders and keep prop
stability for any memoized Textarea component.


const handleAttachmentClick = () => {
fileInputRef.current?.click()
}
Expand Down Expand Up @@ -199,13 +218,12 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ 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' : ''
)}
>
<div
className={cn(
'relative flex items-start w-full',
isMobile && 'mobile-chat-input' // Apply mobile chat input styling
'relative flex items-start w-full'
)}
>
<input type="hidden" name="mapProvider" value={mapProvider} />
Expand Down Expand Up @@ -241,15 +259,16 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ 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)
debouncedGetSuggestions(e.target.value)
}}
onPaste={handlePaste}
onKeyDown={e => {
if (
e.key === 'Enter' &&
Expand Down
4 changes: 2 additions & 2 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const Header = () => {

<div id="header-search-portal" />

<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Expand All @@ -67,7 +67,7 @@ export const Header = () => {
{/* Mobile menu buttons */}
<div className="flex md:hidden gap-2">

<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Expand Down
2 changes: 1 addition & 1 deletion components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
<Button variant="ghost" size="icon" data-testid="mobile-search-button">
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<a href="https://buy.stripe.com/3cIaEX3tRcur9EM7tbasg00" target="_blank" rel="noopener noreferrer">
<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
Expand Down
11 changes: 0 additions & 11 deletions server.log

This file was deleted.