mirror of
https://github.com/stackblitz/bolt.new
synced 2025-02-05 20:46:43 +00:00
Merge pull request #589 from wonderwhy-er/Add-command-detection-to-git-import-flow
Add command detection to git import flow
This commit is contained in:
commit
67f63aaf31
@ -41,6 +41,7 @@ https://thinktank.ottomator.ai
|
|||||||
- ✅ Mobile friendly (@qwikode)
|
- ✅ Mobile friendly (@qwikode)
|
||||||
- ✅ Better prompt enhancing (@SujalXplores)
|
- ✅ Better prompt enhancing (@SujalXplores)
|
||||||
- ✅ Attach images to prompts (@atrokhym)
|
- ✅ 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** - 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** - 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
|
- ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
|
||||||
|
@ -2,6 +2,8 @@ import ignore from 'ignore';
|
|||||||
import { useGit } from '~/lib/hooks/useGit';
|
import { useGit } from '~/lib/hooks/useGit';
|
||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import WithTooltip from '~/components/ui/Tooltip';
|
||||||
|
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
|
||||||
|
import { generateId } from '~/utils/fileUtils';
|
||||||
|
|
||||||
const IGNORE_PATTERNS = [
|
const IGNORE_PATTERNS = [
|
||||||
'node_modules/**',
|
'node_modules/**',
|
||||||
@ -28,7 +30,6 @@ const IGNORE_PATTERNS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ig = ignore().add(IGNORE_PATTERNS);
|
const ig = ignore().add(IGNORE_PATTERNS);
|
||||||
const generateId = () => Math.random().toString(36).substring(2, 15);
|
|
||||||
|
|
||||||
interface GitCloneButtonProps {
|
interface GitCloneButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -52,36 +53,47 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
|||||||
console.log(filePaths);
|
console.log(filePaths);
|
||||||
|
|
||||||
const textDecoder = new TextDecoder('utf-8');
|
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',
|
role: 'assistant',
|
||||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled" >
|
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||||
${filePaths
|
${fileContents
|
||||||
.map((filePath) => {
|
.map(
|
||||||
const { data: content, encoding } = data[filePath];
|
(file) =>
|
||||||
|
`<boltAction type="file" filePath="${file.path}">
|
||||||
if (encoding === 'utf8') {
|
${file.content}
|
||||||
return `<boltAction type="file" filePath="${filePath}">
|
</boltAction>`,
|
||||||
${content}
|
)
|
||||||
</boltAction>`;
|
.join('\n')}
|
||||||
} else if (content instanceof Uint8Array) {
|
</boltArtifact>`,
|
||||||
return `<boltAction type="file" filePath="${filePath}">
|
|
||||||
${textDecoder.decode(content)}
|
|
||||||
</boltAction>`;
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join('\n')}
|
|
||||||
</boltArtifact>`,
|
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
createdAt: new Date(),
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils';
|
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
|
||||||
import { createChatFromFolder } from '../../utils/folderImport';
|
import { createChatFromFolder } from '~/utils/folderImport';
|
||||||
|
|
||||||
interface ImportFolderButtonProps {
|
interface ImportFolderButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -17,12 +17,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
|||||||
|
|
||||||
if (allFiles.length > MAX_FILES) {
|
if (allFiles.length > MAX_FILES) {
|
||||||
toast.error(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
|
const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const loadingToast = toast.loading(`Importing ${folderName}...`);
|
const loadingToast = toast.loading(`Importing ${folderName}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -23,6 +23,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
}
|
}
|
||||||
|
@ -29,10 +29,12 @@ export const isBinaryFile = async (file: File): Promise<boolean> => {
|
|||||||
|
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
const byte = buffer[i];
|
const byte = buffer[i];
|
||||||
|
|
||||||
if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
|
if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,8 +43,11 @@ export const shouldIncludeFile = (path: string): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
|
const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
|
||||||
const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json'));
|
const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json'));
|
||||||
if (!packageJsonFile) return null;
|
|
||||||
|
if (!packageJsonFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await new Promise<string>((resolve, reject) => {
|
const content = await new Promise<string>((resolve, reject) => {
|
||||||
@ -59,29 +64,32 @@ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const detectProjectType = async (files: File[]): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
|
export const detectProjectType = async (
|
||||||
const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name));
|
files: File[],
|
||||||
|
): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
|
||||||
|
const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name));
|
||||||
|
|
||||||
if (hasFile('package.json')) {
|
if (hasFile('package.json')) {
|
||||||
const packageJson = await readPackageJson(files);
|
const packageJson = await readPackageJson(files);
|
||||||
const scripts = packageJson?.scripts || {};
|
const scripts = packageJson?.scripts || {};
|
||||||
|
|
||||||
// Check for preferred commands in priority order
|
// Check for preferred commands in priority order
|
||||||
const preferredCommands = ['dev', 'start', 'preview'];
|
const preferredCommands = ['dev', 'start', 'preview'];
|
||||||
const availableCommand = preferredCommands.find(cmd => scripts[cmd]);
|
const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
|
||||||
|
|
||||||
if (availableCommand) {
|
if (availableCommand) {
|
||||||
return {
|
return {
|
||||||
type: 'Node.js',
|
type: 'Node.js',
|
||||||
setupCommand: `npm install && npm run ${availableCommand}`,
|
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 {
|
return {
|
||||||
type: 'Node.js',
|
type: 'Node.js',
|
||||||
setupCommand: 'npm install',
|
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 {
|
return {
|
||||||
type: 'Static',
|
type: 'Static',
|
||||||
setupCommand: 'npx --yes serve',
|
setupCommand: 'npx --yes serve',
|
||||||
followupMessage: ''
|
followupMessage: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
import { generateId, detectProjectType } from './fileUtils';
|
import { generateId } from './fileUtils';
|
||||||
|
import { detectProjectCommands, createCommandsMessage } from './projectCommands';
|
||||||
|
|
||||||
export const createChatFromFolder = async (
|
export const createChatFromFolder = async (
|
||||||
files: File[],
|
files: File[],
|
||||||
binaryFiles: string[],
|
binaryFiles: string[],
|
||||||
folderName: string
|
folderName: string,
|
||||||
): Promise<Message[]> => {
|
): Promise<Message[]> => {
|
||||||
const fileArtifacts = await Promise.all(
|
const fileArtifacts = await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<{ content: string; path: string }>((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const content = reader.result as string;
|
const content = reader.result as string;
|
||||||
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
|
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
|
||||||
resolve(
|
resolve({
|
||||||
`<boltAction type="file" filePath="${relativePath}">
|
content,
|
||||||
${content}
|
path: relativePath,
|
||||||
</boltAction>`,
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
reader.onerror = reject;
|
reader.onerror = reject;
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@ -25,32 +26,30 @@ ${content}
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const project = await detectProjectType(files);
|
const commands = await detectProjectCommands(fileArtifacts);
|
||||||
const setupCommand = project.setupCommand ? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>` : '';
|
const commandsMessage = createCommandsMessage(commands);
|
||||||
const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : '';
|
|
||||||
|
|
||||||
const binaryFilesMessage = binaryFiles.length > 0
|
const binaryFilesMessage =
|
||||||
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
|
binaryFiles.length > 0
|
||||||
: '';
|
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
|
||||||
|
: '';
|
||||||
|
|
||||||
const assistantMessages: Message[] = [{
|
const filesMessage: Message = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
|
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
|
||||||
|
|
||||||
<boltArtifact id="imported-files" title="Imported Files">
|
<boltArtifact id="imported-files" title="Imported Files">
|
||||||
${fileArtifacts.join('\n\n')}
|
${fileArtifacts
|
||||||
|
.map(
|
||||||
|
(file) => `<boltAction type="file" filePath="${file.path}">
|
||||||
|
${file.content}
|
||||||
|
</boltAction>`,
|
||||||
|
)
|
||||||
|
.join('\n\n')}
|
||||||
</boltArtifact>`,
|
</boltArtifact>`,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
},{
|
};
|
||||||
role: 'assistant',
|
|
||||||
content: `
|
|
||||||
<boltArtifact id="imported-files" title="Imported Files">
|
|
||||||
${setupCommand}
|
|
||||||
</boltArtifact>${followupMessage}`,
|
|
||||||
id: generateId(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
}];
|
|
||||||
|
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@ -59,5 +58,11 @@ ${setupCommand}
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return [ userMessage, ...assistantMessages ];
|
const messages = [userMessage, filesMessage];
|
||||||
|
|
||||||
|
if (commandsMessage) {
|
||||||
|
messages.push(commandsMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
};
|
};
|
||||||
|
80
app/utils/projectCommands.ts
Normal file
80
app/utils/projectCommands.ts
Normal file
@ -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<ProjectCommands> {
|
||||||
|
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: `
|
||||||
|
<boltArtifact id="project-setup" title="Project Setup">
|
||||||
|
<boltAction type="shell">
|
||||||
|
${commands.setupCommand}
|
||||||
|
</boltAction>
|
||||||
|
</boltArtifact>${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`,
|
||||||
|
id: generateId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user