1- import { useMemo , useState } from 'react' ;
1+ import { useMemo , useState , ClipboardEvent } from 'react' ;
22import { useAppContext } from '../utils/app.context' ;
3- import { Message , PendingMessage } from '../utils/types' ;
3+ import { Message , MessageExtra , PendingMessage } from '../utils/types' ;
44import { classNames } from '../utils/misc' ;
55import MarkdownDisplay , { CopyButton } from './MarkdownDisplay' ;
66import {
77 ArrowPathIcon ,
88 ChevronLeftIcon ,
99 ChevronRightIcon ,
10+ PaperClipIcon ,
1011 PencilSquareIcon ,
1112} from '@heroicons/react/24/outline' ;
1213import ChatInputExtraContextItem from './ChatInputExtraContextItem' ;
1314import { BtnWithTooltips } from '../utils/common' ;
15+ import Dropzone from 'react-dropzone' ;
16+ import { useChatExtraContext } from './useChatExtraContext' ;
1417
1518interface SplitMessage {
1619 content : PendingMessage [ 'content' ] ;
@@ -33,12 +36,18 @@ export default function ChatMessage({
3336 siblingCurrIdx : number ;
3437 id ?: string ;
3538 onRegenerateMessage ( msg : Message ) : void ;
36- onEditMessage ( msg : Message , content : string ) : void ;
39+ onEditMessage (
40+ msg : Message ,
41+ content : string ,
42+ extra : MessageExtra [ ] | undefined
43+ ) : void ;
3744 onChangeSibling ( sibling : Message [ 'id' ] ) : void ;
3845 isPending ?: boolean ;
3946} ) {
4047 const { viewingChat, config } = useAppContext ( ) ;
48+ const extraContext = useChatExtraContext ( msg . extra ?? [ ] ) ;
4149 const [ editingContent , setEditingContent ] = useState < string | null > ( null ) ;
50+ const [ isDrag , setIsDrag ] = useState ( false ) ;
4251 const timings = useMemo (
4352 ( ) =>
4453 msg . timings
@@ -107,36 +116,92 @@ export default function ChatMessage({
107116 className = { classNames ( {
108117 'chat-bubble markdown' : true ,
109118 'chat-bubble bg-transparent' : ! isUser ,
119+ 'opacity-50' : isDrag , // simply visual feedback to inform user that the file will be accepted
110120 } ) }
111121 >
112122 { /* textarea for editing message */ }
113123 { editingContent !== null && (
114- < >
115- < textarea
116- dir = "auto"
117- className = "textarea textarea-bordered bg-base-100 text-base-content max-w-2xl w-[calc(90vw-8em)] h-24"
118- value = { editingContent }
119- onChange = { ( e ) => setEditingContent ( e . target . value ) }
120- > </ textarea >
121- < br />
122- < button
123- className = "btn btn-ghost mt-2 mr-2"
124- onClick = { ( ) => setEditingContent ( null ) }
125- >
126- Cancel
127- </ button >
128- < button
129- className = "btn mt-2"
130- onClick = { ( ) => {
131- if ( msg . content !== null ) {
132- setEditingContent ( null ) ;
133- onEditMessage ( msg as Message , editingContent ) ;
134- }
135- } }
136- >
137- Submit
138- </ button >
139- </ >
124+ < Dropzone
125+ noClick
126+ onDrop = { ( files : File [ ] ) => {
127+ setIsDrag ( false ) ;
128+ extraContext . onFileAdded ( files ) ;
129+ } }
130+ onDragEnter = { ( ) => setIsDrag ( true ) }
131+ onDragLeave = { ( ) => setIsDrag ( false ) }
132+ multiple = { true }
133+ >
134+ { ( { getRootProps, getInputProps } ) => (
135+ < div
136+ className = "flex flex-col w-full"
137+ onPasteCapture = { ( e : ClipboardEvent < HTMLInputElement > ) => {
138+ const files = Array . from ( e . clipboardData . items )
139+ . filter ( ( item ) => item . kind === 'file' )
140+ . map ( ( item ) => item . getAsFile ( ) )
141+ . filter ( ( file ) => file !== null ) ;
142+
143+ if ( files . length > 0 ) {
144+ e . preventDefault ( ) ;
145+ extraContext . onFileAdded ( files ) ;
146+ }
147+ } }
148+ { ...getRootProps ( ) }
149+ >
150+ < ChatInputExtraContextItem
151+ items = { extraContext . items }
152+ removeItem = { extraContext . removeItem }
153+ />
154+
155+ < div className = "flex flex-row gap-2 ml-2" >
156+ < textarea
157+ dir = "auto"
158+ className = "textarea textarea-bordered bg-base-100 text-base-content max-w-2xl w-[calc(90vw-8em)] h-24"
159+ value = { editingContent }
160+ onChange = { ( e ) => setEditingContent ( e . target . value ) }
161+ />
162+ < label
163+ htmlFor = "file-upload"
164+ className = { classNames ( {
165+ 'btn w-8 h-8 p-0 rounded-full' : true ,
166+ } ) }
167+ >
168+ < PaperClipIcon className = "h-5 w-5" />
169+ </ label >
170+ < input
171+ id = "file-upload"
172+ type = "file"
173+ className = "hidden"
174+ { ...getInputProps ( ) }
175+ hidden
176+ />
177+ </ div >
178+
179+ < div className = "flex flex-row gap-2 ml-2" >
180+ < button
181+ className = "btn btn-ghost mt-2 mr-2"
182+ onClick = { ( ) => setEditingContent ( null ) }
183+ >
184+ Cancel
185+ </ button >
186+ < button
187+ className = "btn mt-2"
188+ onClick = { ( ) => {
189+ if ( msg . content !== null ) {
190+ setEditingContent ( null ) ;
191+ onEditMessage (
192+ msg as Message ,
193+ editingContent ,
194+ extraContext . items
195+ ) ;
196+ }
197+ } }
198+ >
199+ Submit
200+ </ button >
201+ </ div >
202+ </ div >
203+ ) }
204+ </ Dropzone >
140205 ) }
141206 { /* not editing content, render message */ }
142207 { editingContent === null && (
0 commit comments