77
88import React , { useRef , useEffect , useState , useCallback } from "react" ;
99import { usePersistentState , useDraggablePosition } from "../../hooks" ;
10- import { ChatBoxIds , Model } from "../../utils/types" ;
10+ import { ChatBoxIds , FileData , Model } from "../../utils/types" ;
1111import { EXTENSION_NAME , MESSAGE_TYPES } from "../../../common/constants" ;
1212import { ChatInput } from "./components/ChatInput" ;
1313import { ChatLog } from "./components/ChatLog" ;
@@ -18,23 +18,44 @@ import {useKeepInViewport} from "../../hooks/useKeepInViewport";
1818import { startCapture } from "../../features/capture/captureTool" ;
1919import { RichTextModal } from "./components/RichTextModal" ;
2020import { ReferencesList } from "./components/ReferencesList" ;
21- import { prefixChatBoxId } from "../../utils/utils" ;
21+ import { getLanguageForExtension , prefixChatBoxId } from "../../utils/utils" ;
2222import PromptSvg from '../../assets/eye-solid.svg' ;
2323import CaptureTxtSvg from '../../assets/menu-scale.svg' ;
2424import CaptureImgSvg from '../../assets/media-image.svg' ;
25+ import AttachSvg from '../../assets/attachment.svg' ;
2526import { useTheme } from "../../hooks/useTheme" ;
2627import { polyfillRuntimeSendMessage , polyfillGetTabStorage , polyfillRuntimeConnect , polyfillSetTabStorage } from "../../privilegedAPIs/privilegedAPIs" ;
2728import { browser } from "../../../common/browser" ;
2829import styles from "./ChatBox.scss?inline" ;
2930import { withShadowStyles } from "../../utils/withShadowStyles" ;
3031import { startCaptureImage } from "../../features/capture/captureImageTool" ;
3132import { ImageModal } from "./components/ImageModal" ;
32- import { CAPTURED_IMAGE_TAG , CAPTURED_TAG } from "../../utils/constants" ;
33+ import { ATTACHED_TAG , CAPTURED_IMAGE_TAG , CAPTURED_TAG } from "../../utils/constants" ;
34+ import { FilesModal } from "./components/FilesModal" ;
3335
34- const formatMessage = ( message : string , capturedText : string , capturedImage : string ) => {
35- let newMessage = message . replace ( CAPTURED_IMAGE_TAG , capturedImage ? ( `\n\n` ) : "" ) ;
36+ const formatMessage = ( message : string , { capturedText, capturedImage, attachedFiles} : { capturedText : string , capturedImage : string , attachedFiles : FileData [ ] } ) => {
37+ let newMessage = message . replace (
38+ CAPTURED_IMAGE_TAG ,
39+ capturedImage ? ( `\n\n` ) : ""
40+ ) ;
41+
42+ let attachedFilesContents : string | undefined = "" ;
43+
44+ for ( const attachedFile of attachedFiles ) {
45+ const fileExtension = attachedFile . name . split ( "." ) . pop ( ) ?. toLowerCase ( ) ;
46+ const language = getLanguageForExtension ( fileExtension ) ;
47+
48+ attachedFilesContents += `\n\`\`\`${ language } \n${ attachedFile . content } \n\`\`\`\n` ;
49+ }
50+
51+ if ( attachedFilesContents ) {
52+ newMessage = newMessage . replace ( ATTACHED_TAG , attachedFilesContents ) ;
53+ }
3654
37- newMessage = newMessage . replace ( CAPTURED_TAG , capturedText ? ( "\n```text\n" + capturedText + "\n```\n" ) : "" ) ;
55+ newMessage = newMessage . replace (
56+ CAPTURED_TAG ,
57+ capturedText ? ( "\n```text\n" + capturedText + "\n```\n" ) : ""
58+ ) ;
3859
3960 return newMessage ;
4061}
@@ -70,6 +91,10 @@ export const ChatBox = withShadowStyles(({tabId, chatBoxId, onRemove, coordsOffs
7091 const [ capturedImageModalVisible , setCapturedImageModalVisible ] = useState < boolean > ( false ) ;
7192 const [ promptMessage , setPromptMessage ] = usePersistentState < string > ( "promptMessage" , "" , { tabId, chatBoxId} ) ;
7293 const [ promptModalVisible , setPromptModalVisible ] = useState < boolean > ( false ) ;
94+
95+ const [ attachedFiles , setAttachedFiles ] = usePersistentState < FileData [ ] > ( "attachedFiles" , [ ] , { tabId, chatBoxId} ) ;
96+ const [ attachedFilesModalVisible , setAttachedFilesModalVisible ] = useState < boolean > ( false ) ;
97+
7398 const updateTheme = useTheme ( boxRef ) ;
7499
75100 const fetchModels = useCallback ( ( ) => {
@@ -157,7 +182,7 @@ export const ChatBox = withShadowStyles(({tabId, chatBoxId, onRemove, coordsOffs
157182 const handleSend = async ( ) => {
158183 if ( message . trim ( ) === "" ) return ;
159184
160- const formattedMessage = formatMessage ( message , capturedText , capturedImage ) ;
185+ const formattedMessage = formatMessage ( message , { capturedText, capturedImage, attachedFiles } ) ;
161186
162187 const conversationHistory = chatLog
163188 . filter ( ( msg ) => ! msg . loading )
@@ -264,6 +289,38 @@ export const ChatBox = withShadowStyles(({tabId, chatBoxId, onRemove, coordsOffs
264289 ) ;
265290 } ;
266291
292+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
293+
294+ const handleFileChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
295+ const files = event . target . files ;
296+
297+ if ( ! files ) return ;
298+
299+ if ( files . length > 0 ) {
300+ for ( let i = 0 ; i < files . length ; i ++ ) {
301+ const file = files [ i ] ;
302+ const reader = new FileReader ( ) ;
303+
304+ reader . onload = ( e ) => {
305+ const fileContent = e . target ?. result as string ;
306+
307+ const fileData : FileData = {
308+ name : file . name ,
309+ content : fileContent ,
310+ } ;
311+
312+ setAttachedFiles ( ( prevFiles ) => [ ...prevFiles , fileData ] ) ;
313+ } ;
314+
315+ reader . readAsText ( file ) ;
316+ }
317+
318+ if ( fileInputRef . current ) {
319+ fileInputRef . current . value = "" ;
320+ }
321+ }
322+ } ;
323+
267324 return (
268325 < div
269326 id = { prefixChatBoxId ( chatBoxId ) }
@@ -302,6 +359,14 @@ export const ChatBox = withShadowStyles(({tabId, chatBoxId, onRemove, coordsOffs
302359 }
303360 }
304361 />
362+ < input
363+ ref = { fileInputRef }
364+ type = "file"
365+ multiple
366+ accept = ".txt,.md,.html,.css,.scss,.js,.ts,.tsx,.json,.xml,.csv,.yaml,.yml,.ini,.log,.sh,.sql,.py,.java,.c,.cpp,.h,.bat,.env"
367+ style = { { display : "none" } }
368+ onChange = { handleFileChange }
369+ />
305370 < Tools actions = { [
306371 {
307372 call : ( ) => {
@@ -331,14 +396,23 @@ export const ChatBox = withShadowStyles(({tabId, chatBoxId, onRemove, coordsOffs
331396 label : < > Capture < CaptureImgSvg className = { capturedImage ? "icon" : "" } /> </ > ,
332397 tooltip : "capture image" ,
333398 } ,
399+ {
400+ call : ( ) => {
401+ if ( fileInputRef . current ) {
402+ fileInputRef . current . click ( ) ;
403+ }
404+ } ,
405+ label : < > Attach < AttachSvg className = { attachedFiles . length ? "icon" : "" } /> </ > ,
406+ tooltip : "attach files" ,
407+ } ,
334408 {
335409 call : ( ) => {
336410 setPromptModalVisible ( true ) ;
337411 } ,
338412 label : "Prompt" ,
339413 tooltip : "prompt" ,
340414 icon : promptMessage ? < PromptSvg className = "icon" /> : undefined
341- }
415+ } ,
342416 ] } />
343417 < ReferencesList list = { [
344418 ... ( capturedText ? [
@@ -365,13 +439,30 @@ export const ChatBox = withShadowStyles(({tabId, chatBoxId, onRemove, coordsOffs
365439 }
366440 }
367441 ] : [ ] ) ,
442+ ... ( attachedFiles . length ? [
443+ {
444+ text : ATTACHED_TAG ,
445+ tooltip : "Click to view attached files" ,
446+ onClick : ( ) => {
447+ setAttachedFilesModalVisible ( true ) ;
448+ } ,
449+ onClose : ( ) => {
450+ setAttachedFiles ( [ ] ) ;
451+ }
452+ }
453+ ] : [ ] ) ,
368454
369455 ] } />
370456 < RichTextModal
371457 visible = { capturedModalVisible }
372458 richText = { capturedText }
373459 closeButtonName = "Save"
374460 onUpdate = { ( txt :string ) => { setCapturedText ( txt ) ; setCapturedModalVisible ( false ) ; } } />
461+ < FilesModal
462+ visible = { attachedFilesModalVisible }
463+ files = { attachedFiles }
464+ closeButtonName = "Save"
465+ onUpdate = { ( files : FileData [ ] ) => { setAttachedFiles ( files ) ; setAttachedFilesModalVisible ( false ) ; } } />
375466 < ImageModal
376467 visible = { capturedImageModalVisible }
377468 imageBase64 = { capturedImage }
0 commit comments