-
-
Notifications
You must be signed in to change notification settings - Fork 6
Auto-convert large pasted text to files #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
8ae549a
3495da1
ba36e58
6431b41
8a04d49
10ab3fe
250283e
9a54a27
d5d090d
5b0855e
88731f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<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('') | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a user types some context before pasting large text, this call erases their typed content. Per PR discussion, 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 + 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
Suggested change
🤖 Prompt for AI Agents |
||||||||
| } | ||||||||
| } | ||||||||
|
Comment on lines
80
to
100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The paste threshold ( SuggestionExtract 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionAdd 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converting pasted text into a SuggestionReuse the same max-size validation for pasted-text files before calling 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ♻️ 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 |
||||||||
|
|
||||||||
| const handleAttachmentClick = () => { | ||||||||
| fileInputRef.current?.click() | ||||||||
| } | ||||||||
|
|
@@ -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} /> | ||||||||
|
|
@@ -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' && | ||||||||
|
|
||||||||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.