Eduards commited on
Commit
5b7a2a5
·
1 Parent(s): 9b62edd

Refinement of folder import

Browse files
app/components/chat/ImportFolderButton.tsx CHANGED
@@ -1,102 +1,74 @@
1
- import React from 'react';
2
  import type { Message } from 'ai';
3
  import { toast } from 'react-toastify';
4
- import ignore from 'ignore';
 
5
 
6
  interface ImportFolderButtonProps {
7
  className?: string;
8
  importChat?: (description: string, messages: Message[]) => Promise<void>;
9
  }
10
 
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
-
47
  export 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
- : '';
77
 
78
- const message: Message = {
79
- role: 'assistant',
80
- content: `I'll help you set up these files.${binaryFilesMessage}
81
 
82
- <boltArtifact id="imported-files" title="Imported Files">
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]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
  };
102
 
@@ -108,46 +80,8 @@ ${fileArtifacts.join('\n\n')}
108
  className="hidden"
109
  webkitdirectory=""
110
  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
151
  />
152
  <button
153
  onClick={() => {
@@ -155,9 +89,10 @@ ${fileArtifacts.join('\n\n')}
155
  input?.click();
156
  }}
157
  className={className}
 
158
  >
159
  <div className="i-ph:folder-simple-upload" />
160
- Import Folder
161
  </button>
162
  </>
163
  );
 
1
+ import React, { useState } from 'react';
2
  import type { Message } from 'ai';
3
  import { toast } from 'react-toastify';
4
+ import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils';
5
+ import { createChatFromFolder } from '../../utils/folderImport';
6
 
7
  interface ImportFolderButtonProps {
8
  className?: string;
9
  importChat?: (description: string, messages: Message[]) => Promise<void>;
10
  }
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
13
+ const [isLoading, setIsLoading] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
16
+ const allFiles = Array.from(e.target.files || []);
 
17
 
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
 
25
+ const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
26
+ setIsLoading(true);
27
+ const loadingToast = toast.loading(`Importing ${folderName}...`);
28
+
29
+ try {
30
+ const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
31
+
32
+ if (filteredFiles.length === 0) {
33
+ toast.error('No files found in the selected folder');
34
+ return;
35
+ }
36
+
37
+ const fileChecks = await Promise.all(
38
+ filteredFiles.map(async (file) => ({
39
+ file,
40
+ isBinary: await isBinaryFile(file),
41
+ })),
42
+ );
43
+
44
+ const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file);
45
+ const binaryFilePaths = fileChecks
46
+ .filter((f) => f.isBinary)
47
+ .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
48
+
49
+ if (textFiles.length === 0) {
50
+ toast.error('No text files found in the selected folder');
51
+ return;
52
+ }
53
+
54
+ if (binaryFilePaths.length > 0) {
55
+ toast.info(`Skipping ${binaryFilePaths.length} binary files`);
56
+ }
57
+
58
+ const { userMessage, assistantMessage } = await createChatFromFolder(textFiles, binaryFilePaths, folderName);
59
+
60
+ if (importChat) {
61
+ await importChat(folderName, [userMessage, assistantMessage]);
62
+ }
63
+
64
+ toast.success('Folder imported successfully');
65
+ } catch (error) {
66
+ console.error('Failed to import folder:', error);
67
+ toast.error('Failed to import folder');
68
+ } finally {
69
+ setIsLoading(false);
70
+ toast.dismiss(loadingToast);
71
+ e.target.value = ''; // Reset file input
72
  }
73
  };
74
 
 
80
  className="hidden"
81
  webkitdirectory=""
82
  directory=""
83
+ onChange={handleFileChange}
84
+ {...({} as any)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  />
86
  <button
87
  onClick={() => {
 
89
  input?.click();
90
  }}
91
  className={className}
92
+ disabled={isLoading}
93
  >
94
  <div className="i-ph:folder-simple-upload" />
95
+ {isLoading ? 'Importing...' : 'Import Folder'}
96
  </button>
97
  </>
98
  );
app/utils/fileUtils.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ignore from 'ignore';
2
+
3
+ // Common patterns to ignore, similar to .gitignore
4
+ export const IGNORE_PATTERNS = [
5
+ 'node_modules/**',
6
+ '.git/**',
7
+ 'dist/**',
8
+ 'build/**',
9
+ '.next/**',
10
+ 'coverage/**',
11
+ '.cache/**',
12
+ '.vscode/**',
13
+ '.idea/**',
14
+ '**/*.log',
15
+ '**/.DS_Store',
16
+ '**/npm-debug.log*',
17
+ '**/yarn-debug.log*',
18
+ '**/yarn-error.log*',
19
+ ];
20
+
21
+ export const MAX_FILES = 1000;
22
+ export const ig = ignore().add(IGNORE_PATTERNS);
23
+
24
+ export const generateId = () => Math.random().toString(36).substring(2, 15);
25
+
26
+ export const isBinaryFile = async (file: File): Promise<boolean> => {
27
+ const chunkSize = 1024;
28
+ const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer());
29
+
30
+ for (let i = 0; i < buffer.length; i++) {
31
+ const byte = buffer[i];
32
+ if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ };
38
+
39
+ export const shouldIncludeFile = (path: string): boolean => {
40
+ return !ig.ignores(path);
41
+ };
42
+
43
+ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
44
+ const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json'));
45
+ if (!packageJsonFile) return null;
46
+
47
+ try {
48
+ const content = await new Promise<string>((resolve, reject) => {
49
+ const reader = new FileReader();
50
+ reader.onload = () => resolve(reader.result as string);
51
+ reader.onerror = reject;
52
+ reader.readAsText(packageJsonFile);
53
+ });
54
+
55
+ return JSON.parse(content);
56
+ } catch (error) {
57
+ console.error('Error reading package.json:', error);
58
+ return null;
59
+ }
60
+ };
61
+
62
+ export const detectProjectType = async (files: File[]): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
63
+ const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name));
64
+
65
+ if (hasFile('package.json')) {
66
+ const packageJson = await readPackageJson(files);
67
+ const scripts = packageJson?.scripts || {};
68
+
69
+ // Check for preferred commands in priority order
70
+ const preferredCommands = ['dev', 'start', 'preview'];
71
+ const availableCommand = preferredCommands.find(cmd => scripts[cmd]);
72
+
73
+ if (availableCommand) {
74
+ return {
75
+ type: 'Node.js',
76
+ setupCommand: `npm install && npm run ${availableCommand}`,
77
+ followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`
78
+ };
79
+ }
80
+
81
+ return {
82
+ type: 'Node.js',
83
+ setupCommand: 'npm install',
84
+ followupMessage: 'Would you like me to inspect package.json to determine the available scripts for running this project?'
85
+ };
86
+ }
87
+
88
+ if (hasFile('index.html')) {
89
+ return {
90
+ type: 'Static',
91
+ setupCommand: 'npx --yes serve',
92
+ followupMessage: ''
93
+ };
94
+ }
95
+
96
+ return { type: '', setupCommand: '', followupMessage: '' };
97
+ };
app/utils/folderImport.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { generateId, detectProjectType } from './fileUtils';
3
+
4
+ export const createChatFromFolder = async (
5
+ files: File[],
6
+ binaryFiles: string[],
7
+ folderName: string
8
+ ): Promise<{ userMessage: Message; assistantMessage: Message }> => {
9
+ const fileArtifacts = await Promise.all(
10
+ files.map(async (file) => {
11
+ return new Promise<string>((resolve, reject) => {
12
+ const reader = new FileReader();
13
+ reader.onload = () => {
14
+ const content = reader.result as string;
15
+ const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
16
+ resolve(
17
+ `<boltAction type="file" filePath="${relativePath}">
18
+ ${content}
19
+ </boltAction>`,
20
+ );
21
+ };
22
+ reader.onerror = reject;
23
+ reader.readAsText(file);
24
+ });
25
+ }),
26
+ );
27
+
28
+ const project = await detectProjectType(files);
29
+ const setupCommand = project.setupCommand ? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>` : '';
30
+ const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : '';
31
+
32
+ const binaryFilesMessage = binaryFiles.length > 0
33
+ ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
34
+ : '';
35
+
36
+ const assistantMessage: Message = {
37
+ role: 'assistant',
38
+ content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
39
+
40
+ <boltArtifact id="imported-files" title="Imported Files">
41
+ ${fileArtifacts.join('\n\n')}
42
+ ${setupCommand}
43
+ </boltArtifact>${followupMessage}`,
44
+ id: generateId(),
45
+ createdAt: new Date(),
46
+ };
47
+
48
+ const userMessage: Message = {
49
+ role: 'user',
50
+ id: generateId(),
51
+ content: `Import the "${folderName}" folder`,
52
+ createdAt: new Date(),
53
+ };
54
+
55
+ return { userMessage, assistantMessage };
56
+ };