mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-10 14:13:19 +00:00
# UI V3 Changelog Major updates and improvements in this release: ## Core Changes - Complete NEW REWRITTEN UI system overhaul (V3) with semantic design tokens - New settings management system with drag-and-drop capabilities - Enhanced provider system supporting multiple AI services - Improved theme system with better dark mode support - New component library with consistent design patterns ## Technical Updates - Reorganized project architecture for better maintainability - Performance optimizations and bundle size improvements - Enhanced security features and access controls - Improved developer experience with better tooling - Comprehensive testing infrastructure ## New Features - Background rays effect for improved visual feedback - Advanced tab management system - Automatic and manual update support - Enhanced error handling and visualization - Improved accessibility across all components For detailed information about all changes and improvements, please see the full changelog.
182 lines
5.3 KiB
TypeScript
182 lines
5.3 KiB
TypeScript
import ignore from 'ignore';
|
|
import { useGit } from '~/lib/hooks/useGit';
|
|
import type { Message } from 'ai';
|
|
import { detectProjectCommands, createCommandsMessage, escapeBoltTags } from '~/utils/projectCommands';
|
|
import { generateId } from '~/utils/fileUtils';
|
|
import { useState } from 'react';
|
|
import { toast } from 'react-toastify';
|
|
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
|
import { RepositorySelectionDialog } from '~/components/@settings/tabs/connections/components/RepositorySelectionDialog';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { Button } from '~/components/ui/Button';
|
|
import type { IChatMetadata } from '~/lib/persistence/db';
|
|
|
|
const IGNORE_PATTERNS = [
|
|
'node_modules/**',
|
|
'.git/**',
|
|
'.github/**',
|
|
'.vscode/**',
|
|
'dist/**',
|
|
'build/**',
|
|
'.next/**',
|
|
'coverage/**',
|
|
'.cache/**',
|
|
'.idea/**',
|
|
'**/*.log',
|
|
'**/.DS_Store',
|
|
'**/npm-debug.log*',
|
|
'**/yarn-debug.log*',
|
|
'**/yarn-error.log*',
|
|
'**/*lock.json',
|
|
'**/*lock.yaml',
|
|
];
|
|
|
|
const ig = ignore().add(IGNORE_PATTERNS);
|
|
|
|
const MAX_FILE_SIZE = 100 * 1024; // 100KB limit per file
|
|
const MAX_TOTAL_SIZE = 500 * 1024; // 500KB total limit
|
|
|
|
interface GitCloneButtonProps {
|
|
className?: string;
|
|
importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise<void>;
|
|
}
|
|
|
|
export default function GitCloneButton({ importChat, className }: GitCloneButtonProps) {
|
|
const { ready, gitClone } = useGit();
|
|
const [loading, setLoading] = useState(false);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
|
const handleClone = async (repoUrl: string) => {
|
|
if (!ready) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const { workdir, data } = await gitClone(repoUrl);
|
|
|
|
if (importChat) {
|
|
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
|
const textDecoder = new TextDecoder('utf-8');
|
|
|
|
let totalSize = 0;
|
|
const skippedFiles: string[] = [];
|
|
const fileContents = [];
|
|
|
|
for (const filePath of filePaths) {
|
|
const { data: content, encoding } = data[filePath];
|
|
|
|
// Skip binary files
|
|
if (
|
|
content instanceof Uint8Array &&
|
|
!filePath.match(/\.(txt|md|js|jsx|ts|tsx|json|html|css|scss|less|yml|yaml|xml|svg)$/i)
|
|
) {
|
|
skippedFiles.push(filePath);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const textContent =
|
|
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '';
|
|
|
|
if (!textContent) {
|
|
continue;
|
|
}
|
|
|
|
// Check file size
|
|
const fileSize = new TextEncoder().encode(textContent).length;
|
|
|
|
if (fileSize > MAX_FILE_SIZE) {
|
|
skippedFiles.push(`${filePath} (too large: ${Math.round(fileSize / 1024)}KB)`);
|
|
continue;
|
|
}
|
|
|
|
// Check total size
|
|
if (totalSize + fileSize > MAX_TOTAL_SIZE) {
|
|
skippedFiles.push(`${filePath} (would exceed total size limit)`);
|
|
continue;
|
|
}
|
|
|
|
totalSize += fileSize;
|
|
fileContents.push({
|
|
path: filePath,
|
|
content: textContent,
|
|
});
|
|
} catch (e: any) {
|
|
skippedFiles.push(`${filePath} (error: ${e.message})`);
|
|
}
|
|
}
|
|
|
|
const commands = await detectProjectCommands(fileContents);
|
|
const commandsMessage = createCommandsMessage(commands);
|
|
|
|
const filesMessage: Message = {
|
|
role: 'assistant',
|
|
content: `Cloning the repo ${repoUrl} into ${workdir}
|
|
${
|
|
skippedFiles.length > 0
|
|
? `\nSkipped files (${skippedFiles.length}):
|
|
${skippedFiles.map((f) => `- ${f}`).join('\n')}`
|
|
: ''
|
|
}
|
|
|
|
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
|
${fileContents
|
|
.map(
|
|
(file) =>
|
|
`<boltAction type="file" filePath="${file.path}">
|
|
${escapeBoltTags(file.content)}
|
|
</boltAction>`,
|
|
)
|
|
.join('\n')}
|
|
</boltArtifact>`,
|
|
id: generateId(),
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
const messages = [filesMessage];
|
|
|
|
if (commandsMessage) {
|
|
messages.push(commandsMessage);
|
|
}
|
|
|
|
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error during import:', error);
|
|
toast.error('Failed to import repository');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
onClick={() => setIsDialogOpen(true)}
|
|
title="Clone a Git Repo"
|
|
variant="outline"
|
|
size="lg"
|
|
className={classNames(
|
|
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
|
|
'text-bolt-elements-textPrimary dark:text-white',
|
|
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
|
|
'border-[#E5E5E5] dark:border-[#333333]',
|
|
'h-10 px-4 py-2 min-w-[120px] justify-center',
|
|
'transition-all duration-200 ease-in-out',
|
|
className,
|
|
)}
|
|
disabled={!ready || loading}
|
|
>
|
|
<span className="i-ph:git-branch w-4 h-4" />
|
|
Clone a Git Repo
|
|
</Button>
|
|
|
|
<RepositorySelectionDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} onSelect={handleClone} />
|
|
|
|
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
|
|
</>
|
|
);
|
|
}
|