From 5b7a2a5991844b3b6ea64de2d05b392dae8260b8 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Tue, 26 Nov 2024 10:18:46 +0200 Subject: [PATCH 1/7] Refinement of folder import --- app/components/chat/ImportFolderButton.tsx | 181 +++++++-------------- app/utils/fileUtils.ts | 97 +++++++++++ app/utils/folderImport.ts | 56 +++++++ 3 files changed, 211 insertions(+), 123 deletions(-) create mode 100644 app/utils/fileUtils.ts create mode 100644 app/utils/folderImport.ts diff --git a/app/components/chat/ImportFolderButton.tsx b/app/components/chat/ImportFolderButton.tsx index 5f822ee..558049e 100644 --- a/app/components/chat/ImportFolderButton.tsx +++ b/app/components/chat/ImportFolderButton.tsx @@ -1,102 +1,74 @@ -import React from 'react'; +import React, { useState } from 'react'; import type { Message } from 'ai'; import { toast } from 'react-toastify'; -import ignore from 'ignore'; +import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils'; +import { createChatFromFolder } from '../../utils/folderImport'; interface ImportFolderButtonProps { className?: string; importChat?: (description: string, messages: Message[]) => Promise; } -// Common patterns to ignore, similar to .gitignore -const IGNORE_PATTERNS = [ - 'node_modules/**', - '.git/**', - 'dist/**', - 'build/**', - '.next/**', - 'coverage/**', - '.cache/**', - '.vscode/**', - '.idea/**', - '**/*.log', - '**/.DS_Store', - '**/npm-debug.log*', - '**/yarn-debug.log*', - '**/yarn-error.log*', -]; - -const ig = ignore().add(IGNORE_PATTERNS); -const generateId = () => Math.random().toString(36).substring(2, 15); - -const isBinaryFile = async (file: File): Promise => { - const chunkSize = 1024; // Read the first 1 KB of the file - const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer()); - - for (let i = 0; i < buffer.length; i++) { - const byte = buffer[i]; - - if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) { - return true; // Found a binary character - } - } - - return false; -}; - export const ImportFolderButton: React.FC = ({ className, importChat }) => { - const shouldIncludeFile = (path: string): boolean => { - return !ig.ignores(path); - }; + const [isLoading, setIsLoading] = useState(false); - const createChatFromFolder = async (files: File[], binaryFiles: string[]) => { - const fileArtifacts = await Promise.all( - files.map(async (file) => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); + const handleFileChange = async (e: React.ChangeEvent) => { + const allFiles = Array.from(e.target.files || []); - reader.onload = () => { - const content = reader.result as string; - const relativePath = file.webkitRelativePath.split('/').slice(1).join('/'); - resolve( - ` -${content} -`, - ); - }; - reader.onerror = reject; - reader.readAsText(file); - }); - }), - ); + if (allFiles.length > MAX_FILES) { + toast.error( + `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.` + ); + return; + } - const binaryFilesMessage = - binaryFiles.length > 0 - ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` - : ''; + const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder'; + setIsLoading(true); + const loadingToast = toast.loading(`Importing ${folderName}...`); - const message: Message = { - role: 'assistant', - content: `I'll help you set up these files.${binaryFilesMessage} + try { + const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath)); - -${fileArtifacts.join('\n\n')} -`, - id: generateId(), - createdAt: new Date(), - }; + if (filteredFiles.length === 0) { + toast.error('No files found in the selected folder'); + return; + } - const userMessage: Message = { - role: 'user', - id: generateId(), - content: 'Import my files', - createdAt: new Date(), - }; + const fileChecks = await Promise.all( + filteredFiles.map(async (file) => ({ + file, + isBinary: await isBinaryFile(file), + })), + ); - const description = `Folder Import: ${files[0].webkitRelativePath.split('/')[0]}`; + const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file); + const binaryFilePaths = fileChecks + .filter((f) => f.isBinary) + .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/')); - if (importChat) { - await importChat(description, [userMessage, message]); + if (textFiles.length === 0) { + toast.error('No text files found in the selected folder'); + return; + } + + if (binaryFilePaths.length > 0) { + toast.info(`Skipping ${binaryFilePaths.length} binary files`); + } + + const { userMessage, assistantMessage } = await createChatFromFolder(textFiles, binaryFilePaths, folderName); + + if (importChat) { + await importChat(folderName, [userMessage, assistantMessage]); + } + + toast.success('Folder imported successfully'); + } catch (error) { + console.error('Failed to import folder:', error); + toast.error('Failed to import folder'); + } finally { + setIsLoading(false); + toast.dismiss(loadingToast); + e.target.value = ''; // Reset file input } }; @@ -108,46 +80,8 @@ ${fileArtifacts.join('\n\n')} className="hidden" webkitdirectory="" directory="" - onChange={async (e) => { - const allFiles = Array.from(e.target.files || []); - const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath)); - - if (filteredFiles.length === 0) { - toast.error('No files found in the selected folder'); - return; - } - - try { - const fileChecks = await Promise.all( - filteredFiles.map(async (file) => ({ - file, - isBinary: await isBinaryFile(file), - })), - ); - - const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file); - const binaryFilePaths = fileChecks - .filter((f) => f.isBinary) - .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/')); - - if (textFiles.length === 0) { - toast.error('No text files found in the selected folder'); - return; - } - - if (binaryFilePaths.length > 0) { - toast.info(`Skipping ${binaryFilePaths.length} binary files`); - } - - await createChatFromFolder(textFiles, binaryFilePaths); - } catch (error) { - console.error('Failed to import folder:', error); - toast.error('Failed to import folder'); - } - - e.target.value = ''; // Reset file input - }} - {...({} as any)} // if removed webkitdirectory will throw errors as unknow attribute + onChange={handleFileChange} + {...({} as any)} /> ); diff --git a/app/utils/fileUtils.ts b/app/utils/fileUtils.ts new file mode 100644 index 0000000..f6a52d9 --- /dev/null +++ b/app/utils/fileUtils.ts @@ -0,0 +1,97 @@ +import ignore from 'ignore'; + +// Common patterns to ignore, similar to .gitignore +export const IGNORE_PATTERNS = [ + 'node_modules/**', + '.git/**', + 'dist/**', + 'build/**', + '.next/**', + 'coverage/**', + '.cache/**', + '.vscode/**', + '.idea/**', + '**/*.log', + '**/.DS_Store', + '**/npm-debug.log*', + '**/yarn-debug.log*', + '**/yarn-error.log*', +]; + +export const MAX_FILES = 1000; +export const ig = ignore().add(IGNORE_PATTERNS); + +export const generateId = () => Math.random().toString(36).substring(2, 15); + +export const isBinaryFile = async (file: File): Promise => { + const chunkSize = 1024; + const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer()); + + for (let i = 0; i < buffer.length; i++) { + const byte = buffer[i]; + if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) { + return true; + } + } + return false; +}; + +export const shouldIncludeFile = (path: string): boolean => { + return !ig.ignores(path); +}; + +const readPackageJson = async (files: File[]): Promise<{ scripts?: Record } | null> => { + const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json')); + if (!packageJsonFile) return null; + + try { + const content = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsText(packageJsonFile); + }); + + return JSON.parse(content); + } catch (error) { + console.error('Error reading package.json:', error); + return null; + } +}; + +export const detectProjectType = async (files: File[]): Promise<{ type: string; setupCommand: string; followupMessage: string }> => { + const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name)); + + if (hasFile('package.json')) { + const packageJson = await readPackageJson(files); + const scripts = packageJson?.scripts || {}; + + // Check for preferred commands in priority order + const preferredCommands = ['dev', 'start', 'preview']; + const availableCommand = preferredCommands.find(cmd => scripts[cmd]); + + if (availableCommand) { + return { + type: 'Node.js', + setupCommand: `npm install && npm run ${availableCommand}`, + followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.` + }; + } + + return { + type: 'Node.js', + setupCommand: 'npm install', + followupMessage: 'Would you like me to inspect package.json to determine the available scripts for running this project?' + }; + } + + if (hasFile('index.html')) { + return { + type: 'Static', + setupCommand: 'npx --yes serve', + followupMessage: '' + }; + } + + return { type: '', setupCommand: '', followupMessage: '' }; +}; diff --git a/app/utils/folderImport.ts b/app/utils/folderImport.ts new file mode 100644 index 0000000..2803286 --- /dev/null +++ b/app/utils/folderImport.ts @@ -0,0 +1,56 @@ +import type { Message } from 'ai'; +import { generateId, detectProjectType } from './fileUtils'; + +export const createChatFromFolder = async ( + files: File[], + binaryFiles: string[], + folderName: string +): Promise<{ userMessage: Message; assistantMessage: Message }> => { + const fileArtifacts = await Promise.all( + files.map(async (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const content = reader.result as string; + const relativePath = file.webkitRelativePath.split('/').slice(1).join('/'); + resolve( + ` +${content} +`, + ); + }; + reader.onerror = reject; + reader.readAsText(file); + }); + }), + ); + + const project = await detectProjectType(files); + const setupCommand = project.setupCommand ? `\n\n\n${project.setupCommand}\n` : ''; + const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : ''; + + const binaryFilesMessage = binaryFiles.length > 0 + ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` + : ''; + + const assistantMessage: Message = { + role: 'assistant', + content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage} + + +${fileArtifacts.join('\n\n')} +${setupCommand} +${followupMessage}`, + id: generateId(), + createdAt: new Date(), + }; + + const userMessage: Message = { + role: 'user', + id: generateId(), + content: `Import the "${folderName}" folder`, + createdAt: new Date(), + }; + + return { userMessage, assistantMessage }; +}; From 64814d56c5aaf41791c9d835263ba3d599e27dd5 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Sun, 8 Dec 2024 04:11:34 +0530 Subject: [PATCH 2/7] Update folderImport.ts updated function output --- app/utils/folderImport.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/utils/folderImport.ts b/app/utils/folderImport.ts index 2803286..57cbe31 100644 --- a/app/utils/folderImport.ts +++ b/app/utils/folderImport.ts @@ -5,7 +5,7 @@ export const createChatFromFolder = async ( files: File[], binaryFiles: string[], folderName: string -): Promise<{ userMessage: Message; assistantMessage: Message }> => { +): Promise => { const fileArtifacts = await Promise.all( files.map(async (file) => { return new Promise((resolve, reject) => { @@ -33,17 +33,24 @@ ${content} ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` : ''; - const assistantMessage: Message = { + const assistantMessages: Message[] = [{ role: 'assistant', content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage} ${fileArtifacts.join('\n\n')} +`, + id: generateId(), + createdAt: new Date(), + },{ + role: 'assistant', + content: ` + ${setupCommand} ${followupMessage}`, id: generateId(), createdAt: new Date(), - }; + }]; const userMessage: Message = { role: 'user', @@ -52,5 +59,5 @@ ${setupCommand} createdAt: new Date(), }; - return { userMessage, assistantMessage }; + return [ userMessage, ...assistantMessages ]; }; From 11f93a88004228d4977c36b3830fed83457ef1af Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Sun, 8 Dec 2024 04:12:29 +0530 Subject: [PATCH 3/7] Update ImportFolderButton.tsx added fix --- app/components/chat/ImportFolderButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/chat/ImportFolderButton.tsx b/app/components/chat/ImportFolderButton.tsx index 70e9f1f..e766a71 100644 --- a/app/components/chat/ImportFolderButton.tsx +++ b/app/components/chat/ImportFolderButton.tsx @@ -54,10 +54,10 @@ export const ImportFolderButton: React.FC = ({ classNam toast.info(`Skipping ${binaryFilePaths.length} binary files`); } - const { userMessage, assistantMessage } = await createChatFromFolder(textFiles, binaryFilePaths, folderName); + const messages = await createChatFromFolder(textFiles, binaryFilePaths, folderName); if (importChat) { - await importChat(folderName, [userMessage, assistantMessage]); + await importChat(folderName, [...messages]); } toast.success('Folder imported successfully'); From 6e61a4fb95af1840d9e01a8864d61c7cd975f8af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Dec 2024 09:58:01 +0000 Subject: [PATCH 4/7] chore: update commit hash to 5b6b26bc9ce287e6e351ca443ad0f411d1371a7f --- app/commit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/commit.json b/app/commit.json index a756318..3e54d03 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "95e38e020cc8a4d865172187fc25c94b39806275" } +{ "commit": "5b6b26bc9ce287e6e351ca443ad0f411d1371a7f" } From 823f536a47f9c78b0e7c5fb9f0befeed5a864f6f Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Sun, 8 Dec 2024 12:16:09 +0200 Subject: [PATCH 5/7] Reuse automatic setup commands for git import --- app/components/chat/GitCloneButton.tsx | 60 +++++++++------- app/components/chat/ImportFolderButton.tsx | 8 ++- app/components/chat/SendButton.client.tsx | 1 + app/utils/fileUtils.ts | 28 +++++--- app/utils/folderImport.ts | 57 ++++++++------- app/utils/projectCommands.ts | 80 ++++++++++++++++++++++ 6 files changed, 171 insertions(+), 63 deletions(-) create mode 100644 app/utils/projectCommands.ts diff --git a/app/components/chat/GitCloneButton.tsx b/app/components/chat/GitCloneButton.tsx index ddecdc8..7b7c9f7 100644 --- a/app/components/chat/GitCloneButton.tsx +++ b/app/components/chat/GitCloneButton.tsx @@ -2,6 +2,8 @@ import ignore from 'ignore'; import { useGit } from '~/lib/hooks/useGit'; import type { Message } from 'ai'; import WithTooltip from '~/components/ui/Tooltip'; +import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands'; +import { generateId } from '~/utils/fileUtils'; const IGNORE_PATTERNS = [ 'node_modules/**', @@ -28,7 +30,6 @@ const IGNORE_PATTERNS = [ ]; const ig = ignore().add(IGNORE_PATTERNS); -const generateId = () => Math.random().toString(36).substring(2, 15); interface GitCloneButtonProps { className?: string; @@ -52,36 +53,47 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) { console.log(filePaths); const textDecoder = new TextDecoder('utf-8'); - const message: Message = { + + // Convert files to common format for command detection + const fileContents = filePaths + .map((filePath) => { + const { data: content, encoding } = data[filePath]; + return { + path: filePath, + content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '', + }; + }) + .filter((f) => f.content); + + // Detect and create commands message + const commands = await detectProjectCommands(fileContents); + const commandsMessage = createCommandsMessage(commands); + + // Create files message + const filesMessage: Message = { role: 'assistant', content: `Cloning the repo ${repoUrl} into ${workdir} - - ${filePaths - .map((filePath) => { - const { data: content, encoding } = data[filePath]; - - if (encoding === 'utf8') { - return ` -${content} -`; - } else if (content instanceof Uint8Array) { - return ` -${textDecoder.decode(content)} -`; - } else { - return ''; - } - }) - .join('\n')} - `, + +${fileContents + .map( + (file) => + ` +${file.content} +`, + ) + .join('\n')} +`, id: generateId(), createdAt: new Date(), }; - console.log(JSON.stringify(message)); - importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, [message]); + const messages = [filesMessage]; - // console.log(files); + if (commandsMessage) { + messages.push(commandsMessage); + } + + await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); } } }; diff --git a/app/components/chat/ImportFolderButton.tsx b/app/components/chat/ImportFolderButton.tsx index e766a71..6cbfcac 100644 --- a/app/components/chat/ImportFolderButton.tsx +++ b/app/components/chat/ImportFolderButton.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; import type { Message } from 'ai'; import { toast } from 'react-toastify'; -import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils'; -import { createChatFromFolder } from '../../utils/folderImport'; +import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils'; +import { createChatFromFolder } from '~/utils/folderImport'; interface ImportFolderButtonProps { className?: string; @@ -17,12 +17,14 @@ export const ImportFolderButton: React.FC = ({ classNam if (allFiles.length > MAX_FILES) { toast.error( - `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.` + `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.`, ); return; } + const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder'; setIsLoading(true); + const loadingToast = toast.loading(`Importing ${folderName}...`); try { diff --git a/app/components/chat/SendButton.client.tsx b/app/components/chat/SendButton.client.tsx index c5aa830..389ca3b 100644 --- a/app/components/chat/SendButton.client.tsx +++ b/app/components/chat/SendButton.client.tsx @@ -23,6 +23,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP disabled={disabled} onClick={(event) => { event.preventDefault(); + if (!disabled) { onClick?.(event); } diff --git a/app/utils/fileUtils.ts b/app/utils/fileUtils.ts index f6a52d9..fcf2a01 100644 --- a/app/utils/fileUtils.ts +++ b/app/utils/fileUtils.ts @@ -29,10 +29,12 @@ export const isBinaryFile = async (file: File): Promise => { for (let i = 0; i < buffer.length; i++) { const byte = buffer[i]; + if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) { return true; } } + return false; }; @@ -41,8 +43,11 @@ export const shouldIncludeFile = (path: string): boolean => { }; const readPackageJson = async (files: File[]): Promise<{ scripts?: Record } | null> => { - const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json')); - if (!packageJsonFile) return null; + const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json')); + + if (!packageJsonFile) { + return null; + } try { const content = await new Promise((resolve, reject) => { @@ -59,29 +64,32 @@ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record => { - const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name)); +export const detectProjectType = async ( + files: File[], +): Promise<{ type: string; setupCommand: string; followupMessage: string }> => { + const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name)); if (hasFile('package.json')) { const packageJson = await readPackageJson(files); const scripts = packageJson?.scripts || {}; - + // Check for preferred commands in priority order const preferredCommands = ['dev', 'start', 'preview']; - const availableCommand = preferredCommands.find(cmd => scripts[cmd]); - + const availableCommand = preferredCommands.find((cmd) => scripts[cmd]); + if (availableCommand) { return { type: 'Node.js', setupCommand: `npm install && npm run ${availableCommand}`, - followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.` + followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`, }; } return { type: 'Node.js', setupCommand: 'npm install', - followupMessage: 'Would you like me to inspect package.json to determine the available scripts for running this project?' + followupMessage: + 'Would you like me to inspect package.json to determine the available scripts for running this project?', }; } @@ -89,7 +97,7 @@ export const detectProjectType = async (files: File[]): Promise<{ type: string; return { type: 'Static', setupCommand: 'npx --yes serve', - followupMessage: '' + followupMessage: '', }; } diff --git a/app/utils/folderImport.ts b/app/utils/folderImport.ts index 57cbe31..759df10 100644 --- a/app/utils/folderImport.ts +++ b/app/utils/folderImport.ts @@ -1,23 +1,24 @@ import type { Message } from 'ai'; -import { generateId, detectProjectType } from './fileUtils'; +import { generateId } from './fileUtils'; +import { detectProjectCommands, createCommandsMessage } from './projectCommands'; export const createChatFromFolder = async ( files: File[], binaryFiles: string[], - folderName: string + folderName: string, ): Promise => { const fileArtifacts = await Promise.all( files.map(async (file) => { - return new Promise((resolve, reject) => { + return new Promise<{ content: string; path: string }>((resolve, reject) => { const reader = new FileReader(); + reader.onload = () => { const content = reader.result as string; const relativePath = file.webkitRelativePath.split('/').slice(1).join('/'); - resolve( - ` -${content} -`, - ); + resolve({ + content, + path: relativePath, + }); }; reader.onerror = reject; reader.readAsText(file); @@ -25,32 +26,30 @@ ${content} }), ); - const project = await detectProjectType(files); - const setupCommand = project.setupCommand ? `\n\n\n${project.setupCommand}\n` : ''; - const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : ''; + const commands = await detectProjectCommands(fileArtifacts); + const commandsMessage = createCommandsMessage(commands); - const binaryFilesMessage = binaryFiles.length > 0 - ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` - : ''; + const binaryFilesMessage = + binaryFiles.length > 0 + ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` + : ''; - const assistantMessages: Message[] = [{ + const filesMessage: Message = { role: 'assistant', content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage} -${fileArtifacts.join('\n\n')} +${fileArtifacts + .map( + (file) => ` +${file.content} +`, + ) + .join('\n\n')} `, id: generateId(), createdAt: new Date(), - },{ - role: 'assistant', - content: ` - -${setupCommand} -${followupMessage}`, - id: generateId(), - createdAt: new Date(), - }]; + }; const userMessage: Message = { role: 'user', @@ -59,5 +58,11 @@ ${setupCommand} createdAt: new Date(), }; - return [ userMessage, ...assistantMessages ]; + const messages = [userMessage, filesMessage]; + + if (commandsMessage) { + messages.push(commandsMessage); + } + + return messages; }; diff --git a/app/utils/projectCommands.ts b/app/utils/projectCommands.ts new file mode 100644 index 0000000..050663a --- /dev/null +++ b/app/utils/projectCommands.ts @@ -0,0 +1,80 @@ +import type { Message } from 'ai'; +import { generateId } from './fileUtils'; + +export interface ProjectCommands { + type: string; + setupCommand: string; + followupMessage: string; +} + +interface FileContent { + content: string; + path: string; +} + +export async function detectProjectCommands(files: FileContent[]): Promise { + const hasFile = (name: string) => files.some((f) => f.path.endsWith(name)); + + if (hasFile('package.json')) { + const packageJsonFile = files.find((f) => f.path.endsWith('package.json')); + + if (!packageJsonFile) { + return { type: '', setupCommand: '', followupMessage: '' }; + } + + try { + const packageJson = JSON.parse(packageJsonFile.content); + const scripts = packageJson?.scripts || {}; + + // Check for preferred commands in priority order + const preferredCommands = ['dev', 'start', 'preview']; + const availableCommand = preferredCommands.find((cmd) => scripts[cmd]); + + if (availableCommand) { + return { + type: 'Node.js', + setupCommand: `npm install && npm run ${availableCommand}`, + followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`, + }; + } + + return { + type: 'Node.js', + setupCommand: 'npm install', + followupMessage: + 'Would you like me to inspect package.json to determine the available scripts for running this project?', + }; + } catch (error) { + console.error('Error parsing package.json:', error); + return { type: '', setupCommand: '', followupMessage: '' }; + } + } + + if (hasFile('index.html')) { + return { + type: 'Static', + setupCommand: 'npx --yes serve', + followupMessage: '', + }; + } + + return { type: '', setupCommand: '', followupMessage: '' }; +} + +export function createCommandsMessage(commands: ProjectCommands): Message | null { + if (!commands.setupCommand) { + return null; + } + + return { + role: 'assistant', + content: ` + + +${commands.setupCommand} + +${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`, + id: generateId(), + createdAt: new Date(), + }; +} From de37f6dab84a5dda0fac249bd3cefaec68f71203 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Sun, 8 Dec 2024 12:17:40 +0200 Subject: [PATCH 6/7] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 33f861f..9ac2581 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ https://thinktank.ottomator.ai - ✅ Mobile friendly (@qwikode) - ✅ Better prompt enhancing (@SujalXplores) - ✅ Attach images to prompts (@atrokhym) +- ✅ Detect package.json and commands to auto install and run preview for folder and git import (@wonderwhy-er) - ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs) - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start) - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call From 6921c15943a3bd25900a50bcb8614326dab2a55f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Dec 2024 11:23:12 +0000 Subject: [PATCH 7/7] chore: update commit hash to 67f63aaf31f406379daa97708d6a1a9f8ac41d43 --- app/commit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/commit.json b/app/commit.json index 3e54d03..64fcbce 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "5b6b26bc9ce287e6e351ca443ad0f411d1371a7f" } +{ "commit": "67f63aaf31f406379daa97708d6a1a9f8ac41d43" }