diff --git a/app/components/git/GitUrlImport.client.tsx b/app/components/git/GitUrlImport.client.tsx new file mode 100644 index 00000000..cbdeaa5c --- /dev/null +++ b/app/components/git/GitUrlImport.client.tsx @@ -0,0 +1,117 @@ +import { useSearchParams } from '@remix-run/react'; +import { generateId, type Message } from 'ai'; +import ignore from 'ignore'; +import { useEffect, useState } from 'react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { BaseChat } from '~/components/chat/BaseChat'; +import { Chat } from '~/components/chat/Chat.client'; +import { useGit } from '~/lib/hooks/useGit'; +import { useChatHistory } from '~/lib/persistence'; +import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands'; + +const IGNORE_PATTERNS = [ + 'node_modules/**', + '.git/**', + '.github/**', + '.vscode/**', + '**/*.jpg', + '**/*.jpeg', + '**/*.png', + 'dist/**', + 'build/**', + '.next/**', + 'coverage/**', + '.cache/**', + '.vscode/**', + '.idea/**', + '**/*.log', + '**/.DS_Store', + '**/npm-debug.log*', + '**/yarn-debug.log*', + '**/yarn-error.log*', + '**/*lock.json', + '**/*lock.yaml', +]; + +export function GitUrlImport() { + const [searchParams] = useSearchParams(); + const { ready: historyReady, importChat } = useChatHistory(); + const { ready: gitReady, gitClone } = useGit(); + const [imported, setImported] = useState(false); + + const importRepo = async (repoUrl?: string) => { + if (!gitReady && !historyReady) { + return; + } + + if (repoUrl) { + const ig = ignore().add(IGNORE_PATTERNS); + const { workdir, data } = await gitClone(repoUrl); + + if (importChat) { + const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); + + const textDecoder = new TextDecoder('utf-8'); + + // 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} + +${fileContents + .map( + (file) => + ` +${file.content} +`, + ) + .join('\n')} +`, + id: generateId(), + createdAt: new Date(), + }; + + const messages = [filesMessage]; + + if (commandsMessage) { + messages.push(commandsMessage); + } + + await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); + } + } + }; + + useEffect(() => { + if (!historyReady || !gitReady || imported) { + return; + } + + const url = searchParams.get('url'); + + if (!url) { + window.location.href = '/'; + return; + } + + importRepo(url); + setImported(true); + }, [searchParams, historyReady, gitReady, imported]); + + return }>{() => }; +} diff --git a/app/routes/git.tsx b/app/routes/git.tsx new file mode 100644 index 00000000..aa1689a4 --- /dev/null +++ b/app/routes/git.tsx @@ -0,0 +1,23 @@ +import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; +import { json, type MetaFunction } from '@remix-run/cloudflare'; +import { ClientOnly } from 'remix-utils/client-only'; +import { BaseChat } from '~/components/chat/BaseChat'; +import { GitUrlImport } from '~/components/git/GitUrlImport.client'; +import { Header } from '~/components/header/Header'; + +export const meta: MetaFunction = () => { + return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }]; +}; + +export async function loader(args: LoaderFunctionArgs) { + return json({ url: args.params.url }); +} + +export default function Index() { + return ( +
+
+ }>{() => } +
+ ); +}