Skip to content

Commit e58870a

Browse files
authored
feat(react-ui/chat): paste images from clipboard into chat input (#10428)
The chat input only accepted attachments via the file picker, so users who copied an image from a webpage or a screen region had to first save it to a file before attaching it (#10361). Add an onPaste handler on the input textarea that pulls image items out of the clipboard and routes them through the same staging path as the file picker. The per-file processing in handleFileChange is extracted into a shared processFiles helper so both entry points stay in sync. Clipboard images, which arrive unnamed or as a generic "image.png", are given unique typed names so multiple pastes don't collide, and the default paste is suppressed only when an image is actually attached so normal text paste is unaffected. Closes #10361 Signed-off-by: Anai-Guo <antai12232931@outlook.com>
1 parent 8fab1d2 commit e58870a

1 file changed

Lines changed: 22 additions & 0 deletions

File tree

core/http/react-ui/src/pages/Chat.jsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,27 @@ export default function Chat() {
742742
e.target.value = ''
743743
}, [])
744744

745+
const handlePaste = useCallback(async (e) => {
746+
const items = e.clipboardData?.items
747+
if (!items) return
748+
const images = Array.from(items)
749+
.filter(item => item.kind === 'file' && item.type.startsWith('image/'))
750+
.map(item => item.getAsFile())
751+
.filter(Boolean)
752+
if (images.length === 0) return
753+
// A pasted image attaches as a file rather than inserting into the text.
754+
e.preventDefault()
755+
// Clipboard images arrive unnamed or as a generic "image.png"; give each
756+
// a unique, typed name so multiple pastes don't collide.
757+
const newFiles = await Promise.all(images.map(async (file, i) => {
758+
const name = (file.name && file.name !== 'image.png')
759+
? file.name
760+
: `pasted-image-${i + 1}.${(file.type.split('/')[1] || 'png').replace('+xml', '')}`
761+
return { name, type: file.type, base64: await fileToBase64(file) }
762+
}))
763+
setFiles(prev => [...prev, ...newFiles])
764+
}, [])
765+
745766
const handleSend = useCallback(async () => {
746767
const msg = input.trim()
747768
if (!msg && files.length === 0) return
@@ -1391,6 +1412,7 @@ export default function Chat() {
13911412
value={input}
13921413
onChange={(e) => setInput(e.target.value)}
13931414
onKeyDown={handleKeyDown}
1415+
onPaste={handlePaste}
13941416
placeholder={t('input.placeholder')}
13951417
rows={1}
13961418
disabled={isStreaming}

0 commit comments

Comments
 (0)