codacus commited on
Commit
6d4196a
·
unverified ·
1 Parent(s): df766c9

fix: improve push to github option (#1111)

Browse files

* feat: better push to githubbutton

* added url update on push to github

app/components/chat/GitCloneButton.tsx CHANGED
@@ -6,6 +6,7 @@ import { generateId } from '~/utils/fileUtils';
6
  import { useState } from 'react';
7
  import { toast } from 'react-toastify';
8
  import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
 
9
 
10
  const IGNORE_PATTERNS = [
11
  'node_modules/**',
@@ -35,7 +36,7 @@ const ig = ignore().add(IGNORE_PATTERNS);
35
 
36
  interface GitCloneButtonProps {
37
  className?: string;
38
- importChat?: (description: string, messages: Message[]) => Promise<void>;
39
  }
40
 
41
  export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
@@ -98,7 +99,7 @@ ${file.content}
98
  messages.push(commandsMessage);
99
  }
100
 
101
- await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
102
  }
103
  } catch (error) {
104
  console.error('Error during import:', error);
 
6
  import { useState } from 'react';
7
  import { toast } from 'react-toastify';
8
  import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
+ import type { IChatMetadata } from '~/lib/persistence';
10
 
11
  const IGNORE_PATTERNS = [
12
  'node_modules/**',
 
36
 
37
  interface GitCloneButtonProps {
38
  className?: string;
39
+ importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise<void>;
40
  }
41
 
42
  export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
 
99
  messages.push(commandsMessage);
100
  }
101
 
102
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl });
103
  }
104
  } catch (error) {
105
  console.error('Error during import:', error);
app/components/git/GitUrlImport.client.tsx CHANGED
@@ -94,7 +94,7 @@ ${file.content}
94
  messages.push(commandsMessage);
95
  }
96
 
97
- await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
98
  }
99
  } catch (error) {
100
  console.error('Error during import:', error);
 
94
  messages.push(commandsMessage);
95
  }
96
 
97
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages, { gitUrl: repoUrl });
98
  }
99
  } catch (error) {
100
  console.error('Error during import:', error);
app/components/workbench/Workbench.client.tsx CHANGED
@@ -18,6 +18,7 @@ import { EditorPanel } from './EditorPanel';
18
  import { Preview } from './Preview';
19
  import useViewport from '~/lib/hooks';
20
  import Cookies from 'js-cookie';
 
21
 
22
  interface WorkspaceProps {
23
  chatStarted?: boolean;
@@ -66,6 +67,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
66
  const unsavedFiles = useStore(workbenchStore.unsavedFiles);
67
  const files = useStore(workbenchStore.files);
68
  const selectedView = useStore(workbenchStore.currentView);
 
 
69
 
70
  const isSmallViewport = useViewport(1024);
71
 
@@ -171,18 +174,28 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
171
  <PanelHeaderButton
172
  className="mr-1 text-sm"
173
  onClick={() => {
174
- const repoName = prompt(
175
- 'Please enter a name for your new GitHub repository:',
176
- 'bolt-generated-project',
177
- );
 
 
 
 
 
 
 
 
 
 
178
 
179
  if (!repoName) {
180
  alert('Repository name is required. Push to GitHub cancelled.');
181
  return;
182
  }
183
 
184
- const githubUsername = Cookies.get('githubUsername');
185
- const githubToken = Cookies.get('githubToken');
186
 
187
  if (!githubUsername || !githubToken) {
188
  const usernameInput = prompt('Please enter your GitHub username:');
@@ -193,9 +206,26 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
193
  return;
194
  }
195
 
196
- workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput);
197
- } else {
198
- workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  }
200
  }}
201
  >
 
18
  import { Preview } from './Preview';
19
  import useViewport from '~/lib/hooks';
20
  import Cookies from 'js-cookie';
21
+ import { chatMetadata, useChatHistory } from '~/lib/persistence';
22
 
23
  interface WorkspaceProps {
24
  chatStarted?: boolean;
 
67
  const unsavedFiles = useStore(workbenchStore.unsavedFiles);
68
  const files = useStore(workbenchStore.files);
69
  const selectedView = useStore(workbenchStore.currentView);
70
+ const metadata = useStore(chatMetadata);
71
+ const { updateChatMestaData } = useChatHistory();
72
 
73
  const isSmallViewport = useViewport(1024);
74
 
 
174
  <PanelHeaderButton
175
  className="mr-1 text-sm"
176
  onClick={() => {
177
+ let repoName = metadata?.gitUrl?.split('/').slice(-1)[0]?.replace('.git', '') || null;
178
+ let repoConfirmed: boolean = true;
179
+
180
+ if (repoName) {
181
+ repoConfirmed = confirm(`Do you want to push to the repository ${repoName}?`);
182
+ }
183
+
184
+ if (!repoName || !repoConfirmed) {
185
+ repoName = prompt(
186
+ 'Please enter a name for your new GitHub repository:',
187
+ 'bolt-generated-project',
188
+ );
189
+ } else {
190
+ }
191
 
192
  if (!repoName) {
193
  alert('Repository name is required. Push to GitHub cancelled.');
194
  return;
195
  }
196
 
197
+ let githubUsername = Cookies.get('githubUsername');
198
+ let githubToken = Cookies.get('githubToken');
199
 
200
  if (!githubUsername || !githubToken) {
201
  const usernameInput = prompt('Please enter your GitHub username:');
 
206
  return;
207
  }
208
 
209
+ githubUsername = usernameInput;
210
+ githubToken = tokenInput;
211
+
212
+ Cookies.set('githubUsername', usernameInput);
213
+ Cookies.set('githubToken', tokenInput);
214
+ Cookies.set(
215
+ 'git:github.com',
216
+ JSON.stringify({ username: tokenInput, password: 'x-oauth-basic' }),
217
+ );
218
+ }
219
+
220
+ const commitMessage =
221
+ prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit';
222
+ workbenchStore.pushToGitHub(repoName, commitMessage, githubUsername, githubToken);
223
+
224
+ if (!metadata?.gitUrl) {
225
+ updateChatMestaData({
226
+ ...(metadata || {}),
227
+ gitUrl: `https://github.com/${githubUsername}/${repoName}.git`,
228
+ });
229
  }
230
  }}
231
  >
app/lib/hooks/useGit.ts CHANGED
@@ -92,6 +92,7 @@ export function useGit() {
92
  },
93
  onAuthFailure: (url, _auth) => {
94
  toast.error(`Error Authenticating with ${url.split('/')[2]}`);
 
95
  },
96
  onAuthSuccess: (url, auth) => {
97
  saveGitAuth(url, auth);
@@ -107,6 +108,8 @@ export function useGit() {
107
  return { workdir: webcontainer.workdir, data };
108
  } catch (error) {
109
  console.error('Git clone error:', error);
 
 
110
  throw error;
111
  }
112
  },
 
92
  },
93
  onAuthFailure: (url, _auth) => {
94
  toast.error(`Error Authenticating with ${url.split('/')[2]}`);
95
+ throw `Error Authenticating with ${url.split('/')[2]}`;
96
  },
97
  onAuthSuccess: (url, auth) => {
98
  saveGitAuth(url, auth);
 
108
  return { workdir: webcontainer.workdir, data };
109
  } catch (error) {
110
  console.error('Git clone error:', error);
111
+
112
+ // toast.error(`Git clone error ${(error as any).message||""}`);
113
  throw error;
114
  }
115
  },
app/lib/persistence/db.ts CHANGED
@@ -2,6 +2,11 @@ import type { Message } from 'ai';
2
  import { createScopedLogger } from '~/utils/logger';
3
  import type { ChatHistoryItem } from './useChatHistory';
4
 
 
 
 
 
 
5
  const logger = createScopedLogger('ChatHistory');
6
 
7
  // this is used at the top level and never rejects
@@ -53,6 +58,7 @@ export async function setMessages(
53
  urlId?: string,
54
  description?: string,
55
  timestamp?: string,
 
56
  ): Promise<void> {
57
  return new Promise((resolve, reject) => {
58
  const transaction = db.transaction('chats', 'readwrite');
@@ -69,6 +75,7 @@ export async function setMessages(
69
  urlId,
70
  description,
71
  timestamp: timestamp ?? new Date().toISOString(),
 
72
  });
73
 
74
  request.onsuccess = () => resolve();
@@ -204,6 +211,7 @@ export async function createChatFromMessages(
204
  db: IDBDatabase,
205
  description: string,
206
  messages: Message[],
 
207
  ): Promise<string> {
208
  const newId = await getNextId(db);
209
  const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
@@ -214,6 +222,8 @@ export async function createChatFromMessages(
214
  messages,
215
  newUrlId, // Use the new urlId
216
  description,
 
 
217
  );
218
 
219
  return newUrlId; // Return the urlId instead of id for navigation
@@ -230,5 +240,19 @@ export async function updateChatDescription(db: IDBDatabase, id: string, descrip
230
  throw new Error('Description cannot be empty');
231
  }
232
 
233
- await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  }
 
2
  import { createScopedLogger } from '~/utils/logger';
3
  import type { ChatHistoryItem } from './useChatHistory';
4
 
5
+ export interface IChatMetadata {
6
+ gitUrl: string;
7
+ gitBranch?: string;
8
+ }
9
+
10
  const logger = createScopedLogger('ChatHistory');
11
 
12
  // this is used at the top level and never rejects
 
58
  urlId?: string,
59
  description?: string,
60
  timestamp?: string,
61
+ metadata?: IChatMetadata,
62
  ): Promise<void> {
63
  return new Promise((resolve, reject) => {
64
  const transaction = db.transaction('chats', 'readwrite');
 
75
  urlId,
76
  description,
77
  timestamp: timestamp ?? new Date().toISOString(),
78
+ metadata,
79
  });
80
 
81
  request.onsuccess = () => resolve();
 
211
  db: IDBDatabase,
212
  description: string,
213
  messages: Message[],
214
+ metadata?: IChatMetadata,
215
  ): Promise<string> {
216
  const newId = await getNextId(db);
217
  const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
 
222
  messages,
223
  newUrlId, // Use the new urlId
224
  description,
225
+ undefined, // Use the current timestamp
226
+ metadata,
227
  );
228
 
229
  return newUrlId; // Return the urlId instead of id for navigation
 
240
  throw new Error('Description cannot be empty');
241
  }
242
 
243
+ await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp, chat.metadata);
244
+ }
245
+
246
+ export async function updateChatMetadata(
247
+ db: IDBDatabase,
248
+ id: string,
249
+ metadata: IChatMetadata | undefined,
250
+ ): Promise<void> {
251
+ const chat = await getMessages(db, id);
252
+
253
+ if (!chat) {
254
+ throw new Error('Chat not found');
255
+ }
256
+
257
+ await setMessages(db, id, chat.messages, chat.urlId, chat.description, chat.timestamp, metadata);
258
  }
app/lib/persistence/useChatHistory.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
  setMessages,
14
  duplicateChat,
15
  createChatFromMessages,
 
16
  } from './db';
17
 
18
  export interface ChatHistoryItem {
@@ -21,6 +22,7 @@ export interface ChatHistoryItem {
21
  description?: string;
22
  messages: Message[];
23
  timestamp: string;
 
24
  }
25
 
26
  const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;
@@ -29,7 +31,7 @@ export const db = persistenceEnabled ? await openDatabase() : undefined;
29
 
30
  export const chatId = atom<string | undefined>(undefined);
31
  export const description = atom<string | undefined>(undefined);
32
-
33
  export function useChatHistory() {
34
  const navigate = useNavigate();
35
  const { id: mixedId } = useLoaderData<{ id?: string }>();
@@ -65,6 +67,7 @@ export function useChatHistory() {
65
  setUrlId(storedMessages.urlId);
66
  description.set(storedMessages.description);
67
  chatId.set(storedMessages.id);
 
68
  } else {
69
  navigate('/', { replace: true });
70
  }
@@ -81,6 +84,21 @@ export function useChatHistory() {
81
  return {
82
  ready: !mixedId || ready,
83
  initialMessages,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  storeMessageHistory: async (messages: Message[]) => {
85
  if (!db || messages.length === 0) {
86
  return;
@@ -109,7 +127,7 @@ export function useChatHistory() {
109
  }
110
  }
111
 
112
- await setMessages(db, chatId.get() as string, messages, urlId, description.get());
113
  },
114
  duplicateCurrentChat: async (listItemId: string) => {
115
  if (!db || (!mixedId && !listItemId)) {
@@ -125,13 +143,13 @@ export function useChatHistory() {
125
  console.log(error);
126
  }
127
  },
128
- importChat: async (description: string, messages: Message[]) => {
129
  if (!db) {
130
  return;
131
  }
132
 
133
  try {
134
- const newId = await createChatFromMessages(db, description, messages);
135
  window.location.href = `/chat/${newId}`;
136
  toast.success('Chat imported successfully');
137
  } catch (error) {
 
13
  setMessages,
14
  duplicateChat,
15
  createChatFromMessages,
16
+ type IChatMetadata,
17
  } from './db';
18
 
19
  export interface ChatHistoryItem {
 
22
  description?: string;
23
  messages: Message[];
24
  timestamp: string;
25
+ metadata?: IChatMetadata;
26
  }
27
 
28
  const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;
 
31
 
32
  export const chatId = atom<string | undefined>(undefined);
33
  export const description = atom<string | undefined>(undefined);
34
+ export const chatMetadata = atom<IChatMetadata | undefined>(undefined);
35
  export function useChatHistory() {
36
  const navigate = useNavigate();
37
  const { id: mixedId } = useLoaderData<{ id?: string }>();
 
67
  setUrlId(storedMessages.urlId);
68
  description.set(storedMessages.description);
69
  chatId.set(storedMessages.id);
70
+ chatMetadata.set(storedMessages.metadata);
71
  } else {
72
  navigate('/', { replace: true });
73
  }
 
84
  return {
85
  ready: !mixedId || ready,
86
  initialMessages,
87
+ updateChatMestaData: async (metadata: IChatMetadata) => {
88
+ const id = chatId.get();
89
+
90
+ if (!db || !id) {
91
+ return;
92
+ }
93
+
94
+ try {
95
+ await setMessages(db, id, initialMessages, urlId, description.get(), undefined, metadata);
96
+ chatMetadata.set(metadata);
97
+ } catch (error) {
98
+ toast.error('Failed to update chat metadata');
99
+ console.error(error);
100
+ }
101
+ },
102
  storeMessageHistory: async (messages: Message[]) => {
103
  if (!db || messages.length === 0) {
104
  return;
 
127
  }
128
  }
129
 
130
+ await setMessages(db, chatId.get() as string, messages, urlId, description.get(), undefined, chatMetadata.get());
131
  },
132
  duplicateCurrentChat: async (listItemId: string) => {
133
  if (!db || (!mixedId && !listItemId)) {
 
143
  console.log(error);
144
  }
145
  },
146
+ importChat: async (description: string, messages: Message[], metadata?: IChatMetadata) => {
147
  if (!db) {
148
  return;
149
  }
150
 
151
  try {
152
+ const newId = await createChatFromMessages(db, description, messages, metadata);
153
  window.location.href = `/chat/${newId}`;
154
  toast.success('Chat imported successfully');
155
  } catch (error) {
app/lib/stores/workbench.ts CHANGED
@@ -434,7 +434,7 @@ export class WorkbenchStore {
434
  return syncedFiles;
435
  }
436
 
437
- async pushToGitHub(repoName: string, githubUsername?: string, ghToken?: string) {
438
  try {
439
  // Use cookies if username and token are not provided
440
  const githubToken = ghToken || Cookies.get('githubToken');
@@ -523,7 +523,7 @@ export class WorkbenchStore {
523
  const { data: newCommit } = await octokit.git.createCommit({
524
  owner: repo.owner.login,
525
  repo: repo.name,
526
- message: 'Initial commit from your app',
527
  tree: newTree.sha,
528
  parents: [latestCommitSha],
529
  });
 
434
  return syncedFiles;
435
  }
436
 
437
+ async pushToGitHub(repoName: string, commitMessage?: string, githubUsername?: string, ghToken?: string) {
438
  try {
439
  // Use cookies if username and token are not provided
440
  const githubToken = ghToken || Cookies.get('githubToken');
 
523
  const { data: newCommit } = await octokit.git.createCommit({
524
  owner: repo.owner.login,
525
  repo: repo.name,
526
+ message: commitMessage || 'Initial commit from your app',
527
  tree: newTree.sha,
528
  parents: [latestCommitSha],
529
  });