Eduards commited on
Commit
823f536
·
1 Parent(s): 6e61a4f

Reuse automatic setup commands for git import

Browse files
app/components/chat/GitCloneButton.tsx CHANGED
@@ -2,6 +2,8 @@ import ignore from 'ignore';
2
  import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
4
  import WithTooltip from '~/components/ui/Tooltip';
 
 
5
 
6
  const IGNORE_PATTERNS = [
7
  'node_modules/**',
@@ -28,7 +30,6 @@ const IGNORE_PATTERNS = [
28
  ];
29
 
30
  const ig = ignore().add(IGNORE_PATTERNS);
31
- const generateId = () => Math.random().toString(36).substring(2, 15);
32
 
33
  interface GitCloneButtonProps {
34
  className?: string;
@@ -52,36 +53,47 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
52
  console.log(filePaths);
53
 
54
  const textDecoder = new TextDecoder('utf-8');
55
- const message: Message = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  role: 'assistant',
57
  content: `Cloning the repo ${repoUrl} into ${workdir}
58
- <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled" >
59
- ${filePaths
60
- .map((filePath) => {
61
- const { data: content, encoding } = data[filePath];
62
-
63
- if (encoding === 'utf8') {
64
- return `<boltAction type="file" filePath="${filePath}">
65
- ${content}
66
- </boltAction>`;
67
- } else if (content instanceof Uint8Array) {
68
- return `<boltAction type="file" filePath="${filePath}">
69
- ${textDecoder.decode(content)}
70
- </boltAction>`;
71
- } else {
72
- return '';
73
- }
74
- })
75
- .join('\n')}
76
- </boltArtifact>`,
77
  id: generateId(),
78
  createdAt: new Date(),
79
  };
80
- console.log(JSON.stringify(message));
81
 
82
- importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, [message]);
 
 
 
 
83
 
84
- // console.log(files);
85
  }
86
  }
87
  };
 
2
  import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
4
  import WithTooltip from '~/components/ui/Tooltip';
5
+ import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
6
+ import { generateId } from '~/utils/fileUtils';
7
 
8
  const IGNORE_PATTERNS = [
9
  'node_modules/**',
 
30
  ];
31
 
32
  const ig = ignore().add(IGNORE_PATTERNS);
 
33
 
34
  interface GitCloneButtonProps {
35
  className?: string;
 
53
  console.log(filePaths);
54
 
55
  const textDecoder = new TextDecoder('utf-8');
56
+
57
+ // Convert files to common format for command detection
58
+ const fileContents = filePaths
59
+ .map((filePath) => {
60
+ const { data: content, encoding } = data[filePath];
61
+ return {
62
+ path: filePath,
63
+ content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
64
+ };
65
+ })
66
+ .filter((f) => f.content);
67
+
68
+ // Detect and create commands message
69
+ const commands = await detectProjectCommands(fileContents);
70
+ const commandsMessage = createCommandsMessage(commands);
71
+
72
+ // Create files message
73
+ const filesMessage: Message = {
74
  role: 'assistant',
75
  content: `Cloning the repo ${repoUrl} into ${workdir}
76
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
77
+ ${fileContents
78
+ .map(
79
+ (file) =>
80
+ `<boltAction type="file" filePath="${file.path}">
81
+ ${file.content}
82
+ </boltAction>`,
83
+ )
84
+ .join('\n')}
85
+ </boltArtifact>`,
 
 
 
 
 
 
 
 
 
86
  id: generateId(),
87
  createdAt: new Date(),
88
  };
 
89
 
90
+ const messages = [filesMessage];
91
+
92
+ if (commandsMessage) {
93
+ messages.push(commandsMessage);
94
+ }
95
 
96
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
97
  }
98
  }
99
  };
app/components/chat/ImportFolderButton.tsx CHANGED
@@ -1,8 +1,8 @@
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;
@@ -17,12 +17,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
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
  const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
25
  setIsLoading(true);
 
26
  const loadingToast = toast.loading(`Importing ${folderName}...`);
27
 
28
  try {
 
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;
 
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
+
28
  const loadingToast = toast.loading(`Importing ${folderName}...`);
29
 
30
  try {
app/components/chat/SendButton.client.tsx CHANGED
@@ -23,6 +23,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
23
  disabled={disabled}
24
  onClick={(event) => {
25
  event.preventDefault();
 
26
  if (!disabled) {
27
  onClick?.(event);
28
  }
 
23
  disabled={disabled}
24
  onClick={(event) => {
25
  event.preventDefault();
26
+
27
  if (!disabled) {
28
  onClick?.(event);
29
  }
app/utils/fileUtils.ts CHANGED
@@ -29,10 +29,12 @@ export const isBinaryFile = async (file: File): Promise<boolean> => {
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
 
@@ -41,8 +43,11 @@ export const shouldIncludeFile = (path: string): boolean => {
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) => {
@@ -59,29 +64,32 @@ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string
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
 
@@ -89,7 +97,7 @@ export const detectProjectType = async (files: File[]): Promise<{ type: string;
89
  return {
90
  type: 'Static',
91
  setupCommand: 'npx --yes serve',
92
- followupMessage: ''
93
  };
94
  }
95
 
 
29
 
30
  for (let i = 0; i < buffer.length; i++) {
31
  const byte = buffer[i];
32
+
33
  if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
34
  return true;
35
  }
36
  }
37
+
38
  return false;
39
  };
40
 
 
43
  };
44
 
45
  const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
46
+ const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json'));
47
+
48
+ if (!packageJsonFile) {
49
+ return null;
50
+ }
51
 
52
  try {
53
  const content = await new Promise<string>((resolve, reject) => {
 
64
  }
65
  };
66
 
67
+ export const detectProjectType = async (
68
+ files: File[],
69
+ ): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
70
+ const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name));
71
 
72
  if (hasFile('package.json')) {
73
  const packageJson = await readPackageJson(files);
74
  const scripts = packageJson?.scripts || {};
75
+
76
  // Check for preferred commands in priority order
77
  const preferredCommands = ['dev', 'start', 'preview'];
78
+ const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
79
+
80
  if (availableCommand) {
81
  return {
82
  type: 'Node.js',
83
  setupCommand: `npm install && npm run ${availableCommand}`,
84
+ followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
85
  };
86
  }
87
 
88
  return {
89
  type: 'Node.js',
90
  setupCommand: 'npm install',
91
+ followupMessage:
92
+ 'Would you like me to inspect package.json to determine the available scripts for running this project?',
93
  };
94
  }
95
 
 
97
  return {
98
  type: 'Static',
99
  setupCommand: 'npx --yes serve',
100
+ followupMessage: '',
101
  };
102
  }
103
 
app/utils/folderImport.ts CHANGED
@@ -1,23 +1,24 @@
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<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);
@@ -25,32 +26,30 @@ ${content}
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 assistantMessages: 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
  </boltArtifact>`,
43
  id: generateId(),
44
  createdAt: new Date(),
45
- },{
46
- role: 'assistant',
47
- content: `
48
- <boltArtifact id="imported-files" title="Imported Files">
49
- ${setupCommand}
50
- </boltArtifact>${followupMessage}`,
51
- id: generateId(),
52
- createdAt: new Date(),
53
- }];
54
 
55
  const userMessage: Message = {
56
  role: 'user',
@@ -59,5 +58,11 @@ ${setupCommand}
59
  createdAt: new Date(),
60
  };
61
 
62
- return [ userMessage, ...assistantMessages ];
 
 
 
 
 
 
63
  };
 
1
  import type { Message } from 'ai';
2
+ import { generateId } from './fileUtils';
3
+ import { detectProjectCommands, createCommandsMessage } from './projectCommands';
4
 
5
  export const createChatFromFolder = async (
6
  files: File[],
7
  binaryFiles: string[],
8
+ folderName: string,
9
  ): Promise<Message[]> => {
10
  const fileArtifacts = await Promise.all(
11
  files.map(async (file) => {
12
+ return new Promise<{ content: string; path: string }>((resolve, reject) => {
13
  const reader = new FileReader();
14
+
15
  reader.onload = () => {
16
  const content = reader.result as string;
17
  const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
18
+ resolve({
19
+ content,
20
+ path: relativePath,
21
+ });
 
22
  };
23
  reader.onerror = reject;
24
  reader.readAsText(file);
 
26
  }),
27
  );
28
 
29
+ const commands = await detectProjectCommands(fileArtifacts);
30
+ const commandsMessage = createCommandsMessage(commands);
 
31
 
32
+ const binaryFilesMessage =
33
+ binaryFiles.length > 0
34
+ ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
35
+ : '';
36
 
37
+ const filesMessage: Message = {
38
  role: 'assistant',
39
  content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
40
 
41
  <boltArtifact id="imported-files" title="Imported Files">
42
+ ${fileArtifacts
43
+ .map(
44
+ (file) => `<boltAction type="file" filePath="${file.path}">
45
+ ${file.content}
46
+ </boltAction>`,
47
+ )
48
+ .join('\n\n')}
49
  </boltArtifact>`,
50
  id: generateId(),
51
  createdAt: new Date(),
52
+ };
 
 
 
 
 
 
 
 
53
 
54
  const userMessage: Message = {
55
  role: 'user',
 
58
  createdAt: new Date(),
59
  };
60
 
61
+ const messages = [userMessage, filesMessage];
62
+
63
+ if (commandsMessage) {
64
+ messages.push(commandsMessage);
65
+ }
66
+
67
+ return messages;
68
  };
app/utils/projectCommands.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { generateId } from './fileUtils';
3
+
4
+ export interface ProjectCommands {
5
+ type: string;
6
+ setupCommand: string;
7
+ followupMessage: string;
8
+ }
9
+
10
+ interface FileContent {
11
+ content: string;
12
+ path: string;
13
+ }
14
+
15
+ export async function detectProjectCommands(files: FileContent[]): Promise<ProjectCommands> {
16
+ const hasFile = (name: string) => files.some((f) => f.path.endsWith(name));
17
+
18
+ if (hasFile('package.json')) {
19
+ const packageJsonFile = files.find((f) => f.path.endsWith('package.json'));
20
+
21
+ if (!packageJsonFile) {
22
+ return { type: '', setupCommand: '', followupMessage: '' };
23
+ }
24
+
25
+ try {
26
+ const packageJson = JSON.parse(packageJsonFile.content);
27
+ const scripts = packageJson?.scripts || {};
28
+
29
+ // Check for preferred commands in priority order
30
+ const preferredCommands = ['dev', 'start', 'preview'];
31
+ const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
32
+
33
+ if (availableCommand) {
34
+ return {
35
+ type: 'Node.js',
36
+ setupCommand: `npm install && npm run ${availableCommand}`,
37
+ followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
38
+ };
39
+ }
40
+
41
+ return {
42
+ type: 'Node.js',
43
+ setupCommand: 'npm install',
44
+ followupMessage:
45
+ 'Would you like me to inspect package.json to determine the available scripts for running this project?',
46
+ };
47
+ } catch (error) {
48
+ console.error('Error parsing package.json:', error);
49
+ return { type: '', setupCommand: '', followupMessage: '' };
50
+ }
51
+ }
52
+
53
+ if (hasFile('index.html')) {
54
+ return {
55
+ type: 'Static',
56
+ setupCommand: 'npx --yes serve',
57
+ followupMessage: '',
58
+ };
59
+ }
60
+
61
+ return { type: '', setupCommand: '', followupMessage: '' };
62
+ }
63
+
64
+ export function createCommandsMessage(commands: ProjectCommands): Message | null {
65
+ if (!commands.setupCommand) {
66
+ return null;
67
+ }
68
+
69
+ return {
70
+ role: 'assistant',
71
+ content: `
72
+ <boltArtifact id="project-setup" title="Project Setup">
73
+ <boltAction type="shell">
74
+ ${commands.setupCommand}
75
+ </boltAction>
76
+ </boltArtifact>${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`,
77
+ id: generateId(),
78
+ createdAt: new Date(),
79
+ };
80
+ }