diff --git a/app/components/git/GitUrlImport.client.tsx b/app/components/git/GitUrlImport.client.tsx
new file mode 100644
index 0000000..1321bc2
--- /dev/null
+++ b/app/components/git/GitUrlImport.client.tsx
@@ -0,0 +1,122 @@
+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',
+];
+
+interface GitUrlImportProps {
+ initialUrl?: string;
+}
+
+export function GitUrlImport({ initialUrl }: GitUrlImportProps) {
+ 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;
+ }
+
+ // Use initialUrl if provided, otherwise fallback to URL parameter
+ const url = initialUrl || searchParams.get('url');
+
+ if (!url) {
+ window.location.href = '/';
+ return;
+ }
+
+ importRepo(url);
+ setImported(true);
+ }, [searchParams, historyReady, gitReady, imported, initialUrl]);
+
+ return }>{() => };
+}
\ No newline at end of file
diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts
index 3afa401..935f616 100644
--- a/app/lib/persistence/useChatHistory.ts
+++ b/app/lib/persistence/useChatHistory.ts
@@ -1,10 +1,13 @@
import { useLoaderData, useNavigate } from '@remix-run/react';
import type { Message } from 'ai';
import { atom } from 'nanostores';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { toast } from 'react-toastify';
import { getMessages, getNextId, getUrlId, openDatabase, setMessages } from './db';
import { workbenchStore } from '~/lib/stores/workbench';
+import { createScopedLogger } from '~/utils/logger';
+
+const logger = createScopedLogger('useChatHistory');
export interface ChatHistoryItem {
id: string;
@@ -29,6 +32,43 @@ export function useChatHistory() {
const [ready, setReady] = useState(false);
const [urlId, setUrlId] = useState();
+ const storeMessageHistory = useCallback(async (messages: Message[]) => {
+ if (!db || messages.length === 0) {
+ return;
+ }
+
+ const { firstArtifact } = workbenchStore;
+
+ if (!urlId && firstArtifact?.id) {
+ const urlId = await getUrlId(db, firstArtifact.id);
+
+ navigateChat(urlId);
+ setUrlId(urlId);
+ }
+
+ if (!description.get() && firstArtifact?.title) {
+ description.set(firstArtifact?.title);
+ }
+
+ if (initialMessages.length === 0 && !chatId.get()) {
+ const nextId = await getNextId(db);
+
+ chatId.set(nextId);
+
+ if (!urlId) {
+ navigateChat(nextId);
+ }
+ }
+
+ await setMessages(db, chatId.get() as string, messages, urlId, description.get());
+ }, [initialMessages.length, urlId]);
+
+ const importChat = useCallback(async (chatDescription: string, messages: Message[]) => {
+ logger.trace('Importing chat', { description: chatDescription, messages });
+ description.set(chatDescription);
+ await storeMessageHistory(messages);
+ }, [storeMessageHistory]);
+
useEffect(() => {
if (!db) {
setReady(true);
@@ -58,41 +98,13 @@ export function useChatHistory() {
toast.error(error.message);
});
}
- }, []);
+ }, [mixedId, navigate]);
return {
ready: !mixedId || ready,
initialMessages,
- storeMessageHistory: async (messages: Message[]) => {
- if (!db || messages.length === 0) {
- return;
- }
-
- const { firstArtifact } = workbenchStore;
-
- if (!urlId && firstArtifact?.id) {
- const urlId = await getUrlId(db, firstArtifact.id);
-
- navigateChat(urlId);
- setUrlId(urlId);
- }
-
- if (!description.get() && firstArtifact?.title) {
- description.set(firstArtifact?.title);
- }
-
- if (initialMessages.length === 0 && !chatId.get()) {
- const nextId = await getNextId(db);
-
- chatId.set(nextId);
-
- if (!urlId) {
- navigateChat(nextId);
- }
- }
-
- await setMessages(db, chatId.get() as string, messages, urlId, description.get());
- },
+ storeMessageHistory,
+ importChat
};
}
diff --git a/app/routes/git.tsx b/app/routes/git.tsx
new file mode 100644
index 0000000..2bad25c
--- /dev/null
+++ b/app/routes/git.tsx
@@ -0,0 +1,37 @@
+import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
+import { json, type MetaFunction } from '@remix-run/cloudflare';
+import { useLoaderData } from '@remix-run/react';
+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' }];
+};
+
+interface LoaderData {
+ url: string;
+}
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const url = new URL(request.url);
+ const gitUrl = url.searchParams.get('url');
+
+ if (!gitUrl) {
+ throw new Response('No Git URL provided', { status: 400 });
+ }
+
+ return json({ url: gitUrl });
+}
+
+export default function Index() {
+ const data = useLoaderData();
+
+ return (
+
+
+ }>{() => }
+
+ );
+}
\ No newline at end of file
diff --git a/app/utils/projectCommands.ts b/app/utils/projectCommands.ts
new file mode 100644
index 0000000..cffabb9
--- /dev/null
+++ b/app/utils/projectCommands.ts
@@ -0,0 +1,75 @@
+import { generateId, type Message } from 'ai';
+
+interface FileContent {
+ path: string;
+ content: string;
+}
+
+interface DetectedCommand {
+ type: string;
+ command: string;
+ description: string;
+}
+
+export async function detectProjectCommands(files: FileContent[]): Promise {
+ const commands: DetectedCommand[] = [];
+
+ // Look for package.json to detect npm/node projects
+ const packageJson = files.find(f => f.path === 'package.json');
+ if (packageJson) {
+ try {
+ const pkg = JSON.parse(packageJson.content);
+
+ // Add install command
+ commands.push({
+ type: 'install',
+ command: 'npm install',
+ description: 'Install dependencies'
+ });
+
+ // Add dev command if it exists
+ if (pkg.scripts?.dev) {
+ commands.push({
+ type: 'dev',
+ command: 'npm run dev',
+ description: 'Start development server'
+ });
+ }
+
+ // Add build command if it exists
+ if (pkg.scripts?.build) {
+ commands.push({
+ type: 'build',
+ command: 'npm run build',
+ description: 'Build the project'
+ });
+ }
+ } catch (e) {
+ console.error('Error parsing package.json:', e);
+ }
+ }
+
+ return commands;
+}
+
+export function createCommandsMessage(commands: DetectedCommand[]): Message | null {
+ if (commands.length === 0) {
+ return null;
+ }
+
+ const commandsContent = commands.map(cmd =>
+ `
+${cmd.command}
+`
+ ).join('\n');
+
+ return {
+ role: 'assistant',
+ content: `Here are the available commands for this project:
+
+${commandsContent}
+`,
+ id: generateId(),
+ createdAt: new Date()
+ };
+}
\ No newline at end of file