diff --git a/app/components/chat/GitCloneButton.tsx b/app/components/chat/GitCloneButton.tsx index 376d59d6..43008970 100644 --- a/app/components/chat/GitCloneButton.tsx +++ b/app/components/chat/GitCloneButton.tsx @@ -6,6 +6,7 @@ import { generateId } from '~/utils/fileUtils'; import { useState } from 'react'; import { toast } from 'react-toastify'; import { LoadingOverlay } from '~/components/ui/LoadingOverlay'; +import type { IChatMetadata } from '~/lib/persistence'; const IGNORE_PATTERNS = [ 'node_modules/**', @@ -35,7 +36,7 @@ const ig = ignore().add(IGNORE_PATTERNS); interface GitCloneButtonProps { className?: string; - importChat?: (description: string, messages: Message[]) => Promise; + importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise; } export default function GitCloneButton({ importChat }: GitCloneButtonProps) { @@ -98,7 +99,7 @@ ${file.content} messages.push(commandsMessage); } - await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); + await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl }); } } catch (error) { console.error('Error during import:', error); diff --git a/app/components/git/GitUrlImport.client.tsx b/app/components/git/GitUrlImport.client.tsx index fe8b346b..578c35f2 100644 --- a/app/components/git/GitUrlImport.client.tsx +++ b/app/components/git/GitUrlImport.client.tsx @@ -94,7 +94,7 @@ ${file.content} messages.push(commandsMessage); } - await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); + await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl }); } } catch (error) { console.error('Error during import:', error); diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 0e34b599..be74b772 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -18,6 +18,7 @@ import { EditorPanel } from './EditorPanel'; import { Preview } from './Preview'; import useViewport from '~/lib/hooks'; import Cookies from 'js-cookie'; +import { chatMetadata, useChatHistory } from '~/lib/persistence'; interface WorkspaceProps { chatStarted?: boolean; @@ -66,6 +67,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => const unsavedFiles = useStore(workbenchStore.unsavedFiles); const files = useStore(workbenchStore.files); const selectedView = useStore(workbenchStore.currentView); + const metadata = useStore(chatMetadata); + const { updateChatMestaData } = useChatHistory(); const isSmallViewport = useViewport(1024); @@ -171,18 +174,28 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => { - const repoName = prompt( - 'Please enter a name for your new GitHub repository:', - 'bolt-generated-project', - ); + let repoName = metadata?.gitUrl?.split('/').slice(-1)[0]?.replace('.git', '') || null; + let repoConfirmed: boolean = true; + + if (repoName) { + repoConfirmed = confirm(`Do you want to push to the repository ${repoName}?`); + } + + if (!repoName || !repoConfirmed) { + repoName = prompt( + 'Please enter a name for your new GitHub repository:', + 'bolt-generated-project', + ); + } else { + } if (!repoName) { alert('Repository name is required. Push to GitHub cancelled.'); return; } - const githubUsername = Cookies.get('githubUsername'); - const githubToken = Cookies.get('githubToken'); + let githubUsername = Cookies.get('githubUsername'); + let githubToken = Cookies.get('githubToken'); if (!githubUsername || !githubToken) { const usernameInput = prompt('Please enter your GitHub username:'); @@ -193,9 +206,26 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => return; } - workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput); - } else { - workbenchStore.pushToGitHub(repoName, githubUsername, githubToken); + githubUsername = usernameInput; + githubToken = tokenInput; + + Cookies.set('githubUsername', usernameInput); + Cookies.set('githubToken', tokenInput); + Cookies.set( + 'git:github.com', + JSON.stringify({ username: tokenInput, password: 'x-oauth-basic' }), + ); + } + + const commitMessage = + prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit'; + workbenchStore.pushToGitHub(repoName, commitMessage, githubUsername, githubToken); + + if (!metadata?.gitUrl) { + updateChatMestaData({ + ...(metadata || {}), + gitUrl: `https://github.com/${githubUsername}/${repoName}.git`, + }); } }} > diff --git a/app/lib/hooks/useGit.ts b/app/lib/hooks/useGit.ts index 2efc6e8c..82c650c3 100644 --- a/app/lib/hooks/useGit.ts +++ b/app/lib/hooks/useGit.ts @@ -92,6 +92,7 @@ export function useGit() { }, onAuthFailure: (url, _auth) => { toast.error(`Error Authenticating with ${url.split('/')[2]}`); + throw `Error Authenticating with ${url.split('/')[2]}`; }, onAuthSuccess: (url, auth) => { saveGitAuth(url, auth); @@ -107,6 +108,8 @@ export function useGit() { return { workdir: webcontainer.workdir, data }; } catch (error) { console.error('Git clone error:', error); + + // toast.error(`Git clone error ${(error as any).message||""}`); throw error; } }, diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts index 64aea1cf..2f346f60 100644 --- a/app/lib/persistence/db.ts +++ b/app/lib/persistence/db.ts @@ -2,6 +2,11 @@ import type { Message } from 'ai'; import { createScopedLogger } from '~/utils/logger'; import type { ChatHistoryItem } from './useChatHistory'; +export interface IChatMetadata { + gitUrl: string; + gitBranch?: string; +} + const logger = createScopedLogger('ChatHistory'); // this is used at the top level and never rejects @@ -53,6 +58,7 @@ export async function setMessages( urlId?: string, description?: string, timestamp?: string, + metadata?: IChatMetadata, ): Promise { return new Promise((resolve, reject) => { const transaction = db.transaction('chats', 'readwrite'); @@ -69,6 +75,7 @@ export async function setMessages( urlId, description, timestamp: timestamp ?? new Date().toISOString(), + metadata, }); request.onsuccess = () => resolve(); @@ -204,6 +211,7 @@ export async function createChatFromMessages( db: IDBDatabase, description: string, messages: Message[], + metadata?: IChatMetadata, ): Promise { const newId = await getNextId(db); const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat @@ -214,6 +222,8 @@ export async function createChatFromMessages( messages, newUrlId, // Use the new urlId description, + undefined, // Use the current timestamp + metadata, ); return newUrlId; // Return the urlId instead of id for navigation @@ -230,5 +240,19 @@ export async function updateChatDescription(db: IDBDatabase, id: string, descrip throw new Error('Description cannot be empty'); } - await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp); + await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp, chat.metadata); +} + +export async function updateChatMetadata( + db: IDBDatabase, + id: string, + metadata: IChatMetadata | undefined, +): Promise { + const chat = await getMessages(db, id); + + if (!chat) { + throw new Error('Chat not found'); + } + + await setMessages(db, id, chat.messages, chat.urlId, chat.description, chat.timestamp, metadata); } diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index 0a8eeb58..7baefa56 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -13,6 +13,7 @@ import { setMessages, duplicateChat, createChatFromMessages, + type IChatMetadata, } from './db'; export interface ChatHistoryItem { @@ -21,6 +22,7 @@ export interface ChatHistoryItem { description?: string; messages: Message[]; timestamp: string; + metadata?: IChatMetadata; } const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE; @@ -29,7 +31,7 @@ export const db = persistenceEnabled ? await openDatabase() : undefined; export const chatId = atom(undefined); export const description = atom(undefined); - +export const chatMetadata = atom(undefined); export function useChatHistory() { const navigate = useNavigate(); const { id: mixedId } = useLoaderData<{ id?: string }>(); @@ -65,6 +67,7 @@ export function useChatHistory() { setUrlId(storedMessages.urlId); description.set(storedMessages.description); chatId.set(storedMessages.id); + chatMetadata.set(storedMessages.metadata); } else { navigate('/', { replace: true }); } @@ -81,6 +84,21 @@ export function useChatHistory() { return { ready: !mixedId || ready, initialMessages, + updateChatMestaData: async (metadata: IChatMetadata) => { + const id = chatId.get(); + + if (!db || !id) { + return; + } + + try { + await setMessages(db, id, initialMessages, urlId, description.get(), undefined, metadata); + chatMetadata.set(metadata); + } catch (error) { + toast.error('Failed to update chat metadata'); + console.error(error); + } + }, storeMessageHistory: async (messages: Message[]) => { if (!db || messages.length === 0) { return; @@ -109,7 +127,7 @@ export function useChatHistory() { } } - await setMessages(db, chatId.get() as string, messages, urlId, description.get()); + await setMessages(db, chatId.get() as string, messages, urlId, description.get(), undefined, chatMetadata.get()); }, duplicateCurrentChat: async (listItemId: string) => { if (!db || (!mixedId && !listItemId)) { @@ -125,13 +143,13 @@ export function useChatHistory() { console.log(error); } }, - importChat: async (description: string, messages: Message[]) => { + importChat: async (description: string, messages: Message[], metadata?: IChatMetadata) => { if (!db) { return; } try { - const newId = await createChatFromMessages(db, description, messages); + const newId = await createChatFromMessages(db, description, messages, metadata); window.location.href = `/chat/${newId}`; toast.success('Chat imported successfully'); } catch (error) { diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index 92c3508c..32d5b89f 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -434,7 +434,7 @@ export class WorkbenchStore { return syncedFiles; } - async pushToGitHub(repoName: string, githubUsername?: string, ghToken?: string) { + async pushToGitHub(repoName: string, commitMessage?: string, githubUsername?: string, ghToken?: string) { try { // Use cookies if username and token are not provided const githubToken = ghToken || Cookies.get('githubToken'); @@ -523,7 +523,7 @@ export class WorkbenchStore { const { data: newCommit } = await octokit.git.createCommit({ owner: repo.owner.login, repo: repo.name, - message: 'Initial commit from your app', + message: commitMessage || 'Initial commit from your app', tree: newTree.sha, parents: [latestCommitSha], });