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 {
|
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 &&
|
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
|
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 |
version: 11.2.12([email protected]([email protected]))([email protected])
|
|
|
|
|
|
|
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 |
version: 11.2.12([email protected]([email protected]))([email protected])
|
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 |
+
[email protected]: {}
|
9311 |
+
|
9312 | |
9313 |
|
9314 |