11import { useEffect , useMemo , useState } from 'react' ;
22import { CallbackGeneratedChunk , useAppContext } from '../utils/app.context' ;
33import ChatMessage from './ChatMessage' ;
4- import { CanvasType , Message , PendingMessage } from '../utils/types' ;
4+ import { CanvasType , Message , MessageExtraContext , PendingMessage } from '../utils/types' ;
55import { classNames , cleanCurrentUrl , throttle } from '../utils/misc' ;
66import CanvasPyInterpreter from './CanvasPyInterpreter' ;
77import StorageUtils from '../utils/storage' ;
88import { useVSCodeContext } from '../utils/llama-vscode' ;
99import { useChatTextarea , ChatTextareaApi } from './useChatTextarea.ts' ;
10+ import {
11+ ArrowUpIcon ,
12+ StopIcon ,
13+ PaperClipIcon ,
14+ DocumentTextIcon ,
15+ } from '@heroicons/react/24/solid' ;
16+ import {
17+ ChatExtraContextApi ,
18+ useChatExtraContext ,
19+ } from './useChatExtraContext.tsx' ;
20+ import Dropzone from 'react-dropzone' ;
1021
1122/**
1223 * A message display is a message node with additional information for rendering.
@@ -102,10 +113,8 @@ export default function ChatScreen() {
102113 } = useAppContext ( ) ;
103114
104115 const textarea : ChatTextareaApi = useChatTextarea ( prefilledMsg . content ( ) ) ;
105-
106- const { extraContext, clearExtraContext } = useVSCodeContext ( textarea ) ;
107- // TODO: improve this when we have "upload file" feature
108- const currExtra : Message [ 'extra' ] = extraContext ? [ extraContext ] : undefined ;
116+ const extraContext = useChatExtraContext ( ) ;
117+ useVSCodeContext ( textarea , extraContext ) ;
109118
110119 // keep track of leaf node for rendering
111120 const [ currNodeId , setCurrNodeId ] = useState < number > ( - 1 ) ;
@@ -146,15 +155,15 @@ export default function ChatScreen() {
146155 currConvId ,
147156 lastMsgNodeId ,
148157 lastInpMsg ,
149- currExtra ,
158+ extraContext . items ,
150159 onChunk
151160 ) )
152161 ) {
153162 // restore the input message if failed
154163 textarea . setValue ( lastInpMsg ) ;
155164 }
156165 // OK
157- clearExtraContext ( ) ;
166+ extraContext . clearItems ( ) ;
158167 } ;
159168
160169 // for vscode context
@@ -253,41 +262,13 @@ export default function ChatScreen() {
253262 </ div >
254263
255264 { /* chat input */ }
256- < div className = "flex flex-row items-end pt-8 pb-6 sticky bottom-0 bg-base-100" >
257- < textarea
258- // Default (mobile): Enable vertical resize, overflow auto for scrolling if needed
259- // Large screens (lg:): Disable manual resize, apply max-height for autosize limit
260- className = "textarea textarea-bordered w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60)
261- placeholder = "Type a message (Shift+Enter to add a new line)"
262- ref = { textarea . ref }
263- onInput = { textarea . onInput } // Hook's input handler (will only resize height on lg+ screens)
264- onKeyDown = { ( e ) => {
265- if ( e . nativeEvent . isComposing || e . keyCode === 229 ) return ;
266- if ( e . key === 'Enter' && ! e . shiftKey ) {
267- e . preventDefault ( ) ;
268- sendNewMessage ( ) ;
269- }
270- } }
271- id = "msg-input"
272- dir = "auto"
273- // Set a base height of 2 rows for mobile views
274- // On lg+ screens, the hook will calculate and set the initial height anyway
275- rows = { 2 }
276- > </ textarea >
277-
278- { isGenerating ( currConvId ?? '' ) ? (
279- < button
280- className = "btn btn-neutral ml-2"
281- onClick = { ( ) => stopGenerating ( currConvId ?? '' ) }
282- >
283- Stop
284- </ button >
285- ) : (
286- < button className = "btn btn-primary ml-2" onClick = { sendNewMessage } >
287- Send
288- </ button >
289- ) }
290- </ div >
265+ < ChatInput
266+ textarea = { textarea }
267+ extraContext = { extraContext }
268+ onSend = { sendNewMessage }
269+ onStop = { ( ) => stopGenerating ( currConvId ?? '' ) }
270+ isGenerating = { isGenerating ( currConvId ?? '' ) }
271+ />
291272 </ div >
292273 < div className = "w-full sticky top-[7em] h-[calc(100vh-9em)]" >
293274 { canvasData ?. type === CanvasType . PY_INTERPRETER && (
@@ -297,3 +278,104 @@ export default function ChatScreen() {
297278 </ div >
298279 ) ;
299280}
281+
282+ function ChatInput ( {
283+ textarea,
284+ extraContext,
285+ onSend,
286+ onStop,
287+ isGenerating,
288+ } : {
289+ textarea : ChatTextareaApi ;
290+ extraContext : ChatExtraContextApi ;
291+ onSend : ( ) => void ;
292+ onStop : ( ) => void ;
293+ isGenerating : boolean ;
294+ } ) {
295+ const [ isDrag , setIsDrag ] = useState ( false ) ;
296+
297+ return (
298+ < div
299+ className = { classNames ( {
300+ 'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100' : true ,
301+ 'opacity-50' : isDrag , // simply visual feedback to inform user that the file will be accepted
302+ } ) }
303+ >
304+ < Dropzone
305+ noClick
306+ onDrop = { ( files : File [ ] ) => {
307+ setIsDrag ( false ) ;
308+ extraContext . onFileAdded ( files ) ;
309+ } }
310+ onDragEnter = { ( ) => setIsDrag ( true ) }
311+ onDragLeave = { ( ) => setIsDrag ( false ) }
312+ multiple = { true }
313+ >
314+ { ( { getRootProps, getInputProps } ) => (
315+ < div className = "flex rounded-xl border-1 border-base-content/30 p-3 w-full" { ...getRootProps ( ) } >
316+ < textarea
317+ // Default (mobile): Enable vertical resize, overflow auto for scrolling if needed
318+ // Large screens (lg:): Disable manual resize, apply max-height for autosize limit
319+ className = "text-md outline-none border-none w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60)
320+ placeholder = "Type a message (Shift+Enter to add a new line)"
321+ ref = { textarea . ref }
322+ onInput = { textarea . onInput } // Hook's input handler (will only resize height on lg+ screens)
323+ onKeyDown = { ( e ) => {
324+ if ( e . nativeEvent . isComposing || e . keyCode === 229 ) return ;
325+ if ( e . key === 'Enter' && ! e . shiftKey ) {
326+ e . preventDefault ( ) ;
327+ onSend ( ) ;
328+ }
329+ } }
330+ id = "msg-input"
331+ dir = "auto"
332+ // Set a base height of 2 rows for mobile views
333+ // On lg+ screens, the hook will calculate and set the initial height anyway
334+ rows = { 2 }
335+ > </ textarea >
336+
337+ { /* buttons area */ }
338+ < div className = "flex flex-row gap-2 ml-2" >
339+ < label
340+ htmlFor = "file-upload"
341+ className = "btn w-8 h-8 p-0 rounded-full"
342+ >
343+ < PaperClipIcon className = "h-5 w-5" />
344+ </ label >
345+ < input
346+ id = "file-upload"
347+ type = "file"
348+ className = "hidden"
349+ disabled = { isGenerating }
350+ { ...getInputProps ( ) }
351+ hidden
352+ />
353+ { isGenerating ? (
354+ < button
355+ className = "btn btn-neutral w-8 h-8 p-0 rounded-full"
356+ onClick = { onStop }
357+ >
358+ < StopIcon className = "h-5 w-5" />
359+ </ button >
360+ ) : (
361+ < button
362+ className = "btn btn-primary w-8 h-8 p-0 rounded-full"
363+ onClick = { onSend }
364+ >
365+ < ArrowUpIcon className = "h-5 w-5" />
366+ </ button >
367+ ) }
368+ </ div >
369+ </ div >
370+ ) }
371+ </ Dropzone >
372+ </ div >
373+ ) ;
374+ }
375+
376+ function ChatInputExtraContextItem ( { } : {
377+ idx : number ,
378+ item : MessageExtraContext ,
379+ } ) {
380+
381+ }
0 commit comments