Eduards commited on
Commit
9b62edd
·
unverified ·
2 Parent(s): d41a4ac 15d490d

Merge pull request #413 from wonderwhy-er/Import-folder

Browse files
app/components/chat/BaseChat.tsx CHANGED
@@ -19,7 +19,7 @@ import * as Tooltip from '@radix-ui/react-tooltip';
19
  import styles from './BaseChat.module.scss';
20
  import type { ProviderInfo } from '~/utils/types';
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
- import { ImportButton } from '~/components/chat/chatExportAndImport/ImportButton';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
24
 
25
  // @ts-ignore TODO: Introduce proper types
@@ -307,7 +307,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
307
  </div>
308
  </div>
309
  </div>
310
- {!chatStarted && ImportButton(importChat)}
311
  {!chatStarted && ExamplePrompts(sendMessage)}
312
  </div>
313
  <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
 
19
  import styles from './BaseChat.module.scss';
20
  import type { ProviderInfo } from '~/utils/types';
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
+ import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
24
 
25
  // @ts-ignore TODO: Introduce proper types
 
307
  </div>
308
  </div>
309
  </div>
310
+ {!chatStarted && ImportButtons(importChat)}
311
  {!chatStarted && ExamplePrompts(sendMessage)}
312
  </div>
313
  <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
app/components/chat/ImportFolderButton.tsx ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
103
+ return (
104
+ <>
105
+ <input
106
+ type="file"
107
+ id="folder-import"
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={() => {
154
+ const input = document.getElementById('folder-import');
155
+ input?.click();
156
+ }}
157
+ className={className}
158
+ >
159
+ <div className="i-ph:folder-simple-upload" />
160
+ Import Folder
161
+ </button>
162
+ </>
163
+ );
164
+ };
app/components/chat/chatExportAndImport/{ImportButton.tsx → ImportButtons.tsx} RENAMED
@@ -1,8 +1,9 @@
1
  import type { Message } from 'ai';
2
  import { toast } from 'react-toastify';
3
  import React from 'react';
 
4
 
5
- export function ImportButton(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
6
  return (
7
  <div className="flex flex-col items-center justify-center flex-1 p-4">
8
  <input
@@ -59,6 +60,10 @@ export function ImportButton(importChat: ((description: string, messages: Messag
59
  <div className="i-ph:upload-simple" />
60
  Import Chat
61
  </button>
 
 
 
 
62
  </div>
63
  </div>
64
  </div>
 
1
  import type { Message } from 'ai';
2
  import { toast } from 'react-toastify';
3
  import React from 'react';
4
+ import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
5
 
6
+ export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
7
  return (
8
  <div className="flex flex-col items-center justify-center flex-1 p-4">
9
  <input
 
60
  <div className="i-ph:upload-simple" />
61
  Import Chat
62
  </button>
63
+ <ImportFolderButton
64
+ importChat={importChat}
65
+ className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
66
+ />
67
  </div>
68
  </div>
69
  </div>
package.json CHANGED
@@ -71,6 +71,7 @@
71
  "diff": "^5.2.0",
72
  "file-saver": "^2.0.5",
73
  "framer-motion": "^11.2.12",
 
74
  "isbot": "^4.1.0",
75
  "istextorbinary": "^9.5.0",
76
  "jose": "^5.6.3",
 
71
  "diff": "^5.2.0",
72
  "file-saver": "^2.0.5",
73
  "framer-motion": "^11.2.12",
74
+ "ignore": "^6.0.2",
75
  "isbot": "^4.1.0",
76
  "istextorbinary": "^9.5.0",
77
  "jose": "^5.6.3",
pnpm-lock.yaml CHANGED
@@ -143,6 +143,9 @@ importers:
143
  framer-motion:
144
  specifier: ^11.2.12
145
 
 
 
146
  isbot:
147
  specifier: ^4.1.0
148
  version: 4.4.0
@@ -3407,6 +3410,10 @@ packages:
3407
  resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
3408
  engines: {node: '>= 4'}
3409
 
 
 
 
 
3410
3411
  resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
3412
 
@@ -9300,6 +9307,8 @@ snapshots:
9300
 
9301
9302
 
 
 
9303
9304
 
9305
 
143
  framer-motion:
144
  specifier: ^11.2.12
145
146
+ ignore:
147
+ specifier: ^6.0.2
148
+ version: 6.0.2
149
  isbot:
150
  specifier: ^4.1.0
151
  version: 4.4.0
 
3410
  resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
3411
  engines: {node: '>= 4'}
3412
 
3413
3414
+ resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
3415
+ engines: {node: '>= 4'}
3416
+
3417
3418
  resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
3419
 
 
9307
 
9308
9309
 
9310
9311
+
9312
9313
 
9314