1- import React from 'react' ;
1+ import React , { useState } from 'react' ;
22import type { Message } from 'ai' ;
33import { toast } from 'react-toastify' ;
4- import ignore from 'ignore' ;
4+ import { MAX_FILES , isBinaryFile , shouldIncludeFile } from '../../utils/fileUtils' ;
5+ import { createChatFromFolder } from '../../utils/folderImport' ;
56
67interface ImportFolderButtonProps {
78 className ?: string ;
89 importChat ?: ( description : string , messages : Message [ ] ) => Promise < void > ;
910}
1011
11- // Common patterns to ignore, similar to .gitignore
12- const IGNORE_PATTERNS = [
13- 'node_modules/**' ,
14- '.git/**' ,
15- 'dist/**' ,
16- 'build/**' ,
17- '.next/**' ,
18- 'coverage/**' ,
19- '.cache/**' ,
20- '.vscode/**' ,
21- '.idea/**' ,
22- '**/*.log' ,
23- '**/.DS_Store' ,
24- '**/npm-debug.log*' ,
25- '**/yarn-debug.log*' ,
26- '**/yarn-error.log*' ,
27- ] ;
28-
29- const ig = ignore ( ) . add ( IGNORE_PATTERNS ) ;
30- const generateId = ( ) => Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
31-
32- const isBinaryFile = async ( file : File ) : Promise < boolean > => {
33- const chunkSize = 1024 ; // Read the first 1 KB of the file
34- const buffer = new Uint8Array ( await file . slice ( 0 , chunkSize ) . arrayBuffer ( ) ) ;
35-
36- for ( let i = 0 ; i < buffer . length ; i ++ ) {
37- const byte = buffer [ i ] ;
38-
39- if ( byte === 0 || ( byte < 32 && byte !== 9 && byte !== 10 && byte !== 13 ) ) {
40- return true ; // Found a binary character
41- }
42- }
43-
44- return false ;
45- } ;
46-
4712export const ImportFolderButton : React . FC < ImportFolderButtonProps > = ( { className, importChat } ) => {
48- const shouldIncludeFile = ( path : string ) : boolean => {
49- return ! ig . ignores ( path ) ;
50- } ;
51-
52- const createChatFromFolder = async ( files : File [ ] , binaryFiles : string [ ] ) => {
53- const fileArtifacts = await Promise . all (
54- files . map ( async ( file ) => {
55- return new Promise < string > ( ( resolve , reject ) => {
56- const reader = new FileReader ( ) ;
57-
58- reader . onload = ( ) => {
59- const content = reader . result as string ;
60- const relativePath = file . webkitRelativePath . split ( '/' ) . slice ( 1 ) . join ( '/' ) ;
61- resolve (
62- `<boltAction type="file" filePath="${ relativePath } ">
63- ${ content }
64- </boltAction>` ,
65- ) ;
66- } ;
67- reader . onerror = reject ;
68- reader . readAsText ( file ) ;
69- } ) ;
70- } ) ,
71- ) ;
72-
73- const binaryFilesMessage =
74- binaryFiles . length > 0
75- ? `\n\nSkipped ${ binaryFiles . length } binary files:\n${ binaryFiles . map ( ( f ) => `- ${ f } ` ) . join ( '\n' ) } `
76- : '' ;
13+ const [ isLoading , setIsLoading ] = useState ( false ) ;
7714
78- const message : Message = {
79- role : 'assistant' ,
80- content : `I'll help you set up these files.${ binaryFilesMessage }
15+ const handleFileChange = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
16+ const allFiles = Array . from ( e . target . files || [ ] ) ;
8117
82- <boltArtifact id="imported-files" title="Imported Files" type="bundled">
83- ${ fileArtifacts . join ( '\n\n' ) }
84- </boltArtifact>` ,
85- id : generateId ( ) ,
86- createdAt : new Date ( ) ,
87- } ;
88-
89- const userMessage : Message = {
90- role : 'user' ,
91- id : generateId ( ) ,
92- content : 'Import my files' ,
93- createdAt : new Date ( ) ,
94- } ;
95-
96- const description = `Folder Import: ${ files [ 0 ] . webkitRelativePath . split ( '/' ) [ 0 ] } ` ;
97-
98- if ( importChat ) {
99- await importChat ( description , [ userMessage , message ] ) ;
18+ if ( allFiles . length > MAX_FILES ) {
19+ toast . error (
20+ `This folder contains ${ allFiles . length . toLocaleString ( ) } files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${ MAX_FILES . toLocaleString ( ) } files.`
21+ ) ;
22+ return ;
23+ }
24+ const folderName = allFiles [ 0 ] ?. webkitRelativePath . split ( '/' ) [ 0 ] || 'Unknown Folder' ;
25+ setIsLoading ( true ) ;
26+ const loadingToast = toast . loading ( `Importing ${ folderName } ...` ) ;
27+
28+ try {
29+ const filteredFiles = allFiles . filter ( ( file ) => shouldIncludeFile ( file . webkitRelativePath ) ) ;
30+
31+ if ( filteredFiles . length === 0 ) {
32+ toast . error ( 'No files found in the selected folder' ) ;
33+ return ;
34+ }
35+
36+ const fileChecks = await Promise . all (
37+ filteredFiles . map ( async ( file ) => ( {
38+ file,
39+ isBinary : await isBinaryFile ( file ) ,
40+ } ) ) ,
41+ ) ;
42+
43+ const textFiles = fileChecks . filter ( ( f ) => ! f . isBinary ) . map ( ( f ) => f . file ) ;
44+ const binaryFilePaths = fileChecks
45+ . filter ( ( f ) => f . isBinary )
46+ . map ( ( f ) => f . file . webkitRelativePath . split ( '/' ) . slice ( 1 ) . join ( '/' ) ) ;
47+
48+ if ( textFiles . length === 0 ) {
49+ toast . error ( 'No text files found in the selected folder' ) ;
50+ return ;
51+ }
52+
53+ if ( binaryFilePaths . length > 0 ) {
54+ toast . info ( `Skipping ${ binaryFilePaths . length } binary files` ) ;
55+ }
56+
57+ const messages = await createChatFromFolder ( textFiles , binaryFilePaths , folderName ) ;
58+
59+ if ( importChat ) {
60+ await importChat ( folderName , [ ...messages ] ) ;
61+ }
62+
63+ toast . success ( 'Folder imported successfully' ) ;
64+ } catch ( error ) {
65+ console . error ( 'Failed to import folder:' , error ) ;
66+ toast . error ( 'Failed to import folder' ) ;
67+ } finally {
68+ setIsLoading ( false ) ;
69+ toast . dismiss ( loadingToast ) ;
70+ e . target . value = '' ; // Reset file input
10071 }
10172 } ;
10273
@@ -108,56 +79,19 @@ ${fileArtifacts.join('\n\n')}
10879 className = "hidden"
10980 webkitdirectory = ""
11081 directory = ""
111- onChange = { async ( e ) => {
112- const allFiles = Array . from ( e . target . files || [ ] ) ;
113- const filteredFiles = allFiles . filter ( ( file ) => shouldIncludeFile ( file . webkitRelativePath ) ) ;
114-
115- if ( filteredFiles . length === 0 ) {
116- toast . error ( 'No files found in the selected folder' ) ;
117- return ;
118- }
119-
120- try {
121- const fileChecks = await Promise . all (
122- filteredFiles . map ( async ( file ) => ( {
123- file,
124- isBinary : await isBinaryFile ( file ) ,
125- } ) ) ,
126- ) ;
127-
128- const textFiles = fileChecks . filter ( ( f ) => ! f . isBinary ) . map ( ( f ) => f . file ) ;
129- const binaryFilePaths = fileChecks
130- . filter ( ( f ) => f . isBinary )
131- . map ( ( f ) => f . file . webkitRelativePath . split ( '/' ) . slice ( 1 ) . join ( '/' ) ) ;
132-
133- if ( textFiles . length === 0 ) {
134- toast . error ( 'No text files found in the selected folder' ) ;
135- return ;
136- }
137-
138- if ( binaryFilePaths . length > 0 ) {
139- toast . info ( `Skipping ${ binaryFilePaths . length } binary files` ) ;
140- }
141-
142- await createChatFromFolder ( textFiles , binaryFilePaths ) ;
143- } catch ( error ) {
144- console . error ( 'Failed to import folder:' , error ) ;
145- toast . error ( 'Failed to import folder' ) ;
146- }
147-
148- e . target . value = '' ; // Reset file input
149- } }
150- { ...( { } as any ) } // if removed webkitdirectory will throw errors as unknow attribute
82+ onChange = { handleFileChange }
83+ { ...( { } as any ) }
15184 />
15285 < button
15386 onClick = { ( ) => {
15487 const input = document . getElementById ( 'folder-import' ) ;
15588 input ?. click ( ) ;
15689 } }
15790 className = { className }
91+ disabled = { isLoading }
15892 >
15993 < div className = "i-ph:upload-simple" />
160- Import Folder
94+ { isLoading ? 'Importing...' : ' Import Folder' }
16195 </ button >
16296 </ >
16397 ) ;
0 commit comments