Cole Medin commited on
Commit
d40be24
·
unverified ·
2 Parent(s): 5a10894 368022d

Merge pull request #30 from muzafferkadir/main

Browse files

feat: added sync files to selected local folder function is created.Yarn package manager fixes, styling fixes. Sass module fix. Added Claude model for open router.

app/components/workbench/Workbench.client.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { useStore } from '@nanostores/react';
2
  import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
3
  import { computed } from 'nanostores';
4
- import { memo, useCallback, useEffect } from 'react';
5
  import { toast } from 'react-toastify';
6
  import {
7
  type OnChangeCallback as OnEditorChange,
@@ -55,6 +55,8 @@ const workbenchVariants = {
55
  export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {
56
  renderLogger.trace('Workbench');
57
 
 
 
58
  const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
59
  const showWorkbench = useStore(workbenchStore.showWorkbench);
60
  const selectedFile = useStore(workbenchStore.selectedFile);
@@ -99,6 +101,21 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
99
  workbenchStore.resetCurrentDocument();
100
  }, []);
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  return (
103
  chatStarted && (
104
  <motion.div
@@ -132,6 +149,10 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
132
  <div className="i-ph:code" />
133
  Download Code
134
  </PanelHeaderButton>
 
 
 
 
135
  <PanelHeaderButton
136
  className="mr-1 text-sm"
137
  onClick={() => {
@@ -209,7 +230,6 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
209
  )
210
  );
211
  });
212
-
213
  interface ViewProps extends HTMLMotionProps<'div'> {
214
  children: JSX.Element;
215
  }
 
1
  import { useStore } from '@nanostores/react';
2
  import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
3
  import { computed } from 'nanostores';
4
+ import { memo, useCallback, useEffect, useState } from 'react';
5
  import { toast } from 'react-toastify';
6
  import {
7
  type OnChangeCallback as OnEditorChange,
 
55
  export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {
56
  renderLogger.trace('Workbench');
57
 
58
+ const [isSyncing, setIsSyncing] = useState(false);
59
+
60
  const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
61
  const showWorkbench = useStore(workbenchStore.showWorkbench);
62
  const selectedFile = useStore(workbenchStore.selectedFile);
 
101
  workbenchStore.resetCurrentDocument();
102
  }, []);
103
 
104
+ const handleSyncFiles = useCallback(async () => {
105
+ setIsSyncing(true);
106
+
107
+ try {
108
+ const directoryHandle = await window.showDirectoryPicker();
109
+ await workbenchStore.syncFiles(directoryHandle);
110
+ toast.success('Files synced successfully');
111
+ } catch (error) {
112
+ console.error('Error syncing files:', error);
113
+ toast.error('Failed to sync files');
114
+ } finally {
115
+ setIsSyncing(false);
116
+ }
117
+ }, []);
118
+
119
  return (
120
  chatStarted && (
121
  <motion.div
 
149
  <div className="i-ph:code" />
150
  Download Code
151
  </PanelHeaderButton>
152
+ <PanelHeaderButton className="mr-1 text-sm" onClick={handleSyncFiles} disabled={isSyncing}>
153
+ {isSyncing ? <div className="i-ph:spinner" /> : <div className="i-ph:cloud-arrow-down" />}
154
+ {isSyncing ? 'Syncing...' : 'Sync Files'}
155
+ </PanelHeaderButton>
156
  <PanelHeaderButton
157
  className="mr-1 text-sm"
158
  onClick={() => {
 
230
  )
231
  );
232
  });
 
233
  interface ViewProps extends HTMLMotionProps<'div'> {
234
  children: JSX.Element;
235
  }
app/lib/stores/workbench.ts CHANGED
@@ -281,21 +281,22 @@ export class WorkbenchStore {
281
 
282
  for (const [filePath, dirent] of Object.entries(files)) {
283
  if (dirent?.type === 'file' && !dirent.isBinary) {
284
- // Remove '/home/project/' from the beginning of the path
285
  const relativePath = filePath.replace(/^\/home\/project\//, '');
286
 
287
- // Split the path into segments
288
  const pathSegments = relativePath.split('/');
289
 
290
- // If there's more than one segment, we need to create folders
291
  if (pathSegments.length > 1) {
292
  let currentFolder = zip;
 
293
  for (let i = 0; i < pathSegments.length - 1; i++) {
294
  currentFolder = currentFolder.folder(pathSegments[i])!;
295
  }
296
  currentFolder.file(pathSegments[pathSegments.length - 1], dirent.content);
297
  } else {
298
- // If there's only one segment, it's a file in the root
299
  zip.file(relativePath, dirent.content);
300
  }
301
  }
@@ -305,6 +306,35 @@ export class WorkbenchStore {
305
  saveAs(content, 'project.zip');
306
  }
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
309
 
310
  try {
 
281
 
282
  for (const [filePath, dirent] of Object.entries(files)) {
283
  if (dirent?.type === 'file' && !dirent.isBinary) {
284
+ // remove '/home/project/' from the beginning of the path
285
  const relativePath = filePath.replace(/^\/home\/project\//, '');
286
 
287
+ // split the path into segments
288
  const pathSegments = relativePath.split('/');
289
 
290
+ // if there's more than one segment, we need to create folders
291
  if (pathSegments.length > 1) {
292
  let currentFolder = zip;
293
+
294
  for (let i = 0; i < pathSegments.length - 1; i++) {
295
  currentFolder = currentFolder.folder(pathSegments[i])!;
296
  }
297
  currentFolder.file(pathSegments[pathSegments.length - 1], dirent.content);
298
  } else {
299
+ // if there's only one segment, it's a file in the root
300
  zip.file(relativePath, dirent.content);
301
  }
302
  }
 
306
  saveAs(content, 'project.zip');
307
  }
308
 
309
+ async syncFiles(targetHandle: FileSystemDirectoryHandle) {
310
+ const files = this.files.get();
311
+ const syncedFiles = [];
312
+
313
+ for (const [filePath, dirent] of Object.entries(files)) {
314
+ if (dirent?.type === 'file' && !dirent.isBinary) {
315
+ const relativePath = filePath.replace(/^\/home\/project\//, '');
316
+ const pathSegments = relativePath.split('/');
317
+ let currentHandle = targetHandle;
318
+
319
+ for (let i = 0; i < pathSegments.length - 1; i++) {
320
+ currentHandle = await currentHandle.getDirectoryHandle(pathSegments[i], { create: true });
321
+ }
322
+
323
+ // create or get the file
324
+ const fileHandle = await currentHandle.getFileHandle(pathSegments[pathSegments.length - 1], { create: true });
325
+
326
+ // write the file content
327
+ const writable = await fileHandle.createWritable();
328
+ await writable.write(dirent.content);
329
+ await writable.close();
330
+
331
+ syncedFiles.push(relativePath);
332
+ }
333
+ }
334
+
335
+ return syncedFiles;
336
+ }
337
+
338
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
339
 
340
  try {
app/types/global.d.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ interface Window {
2
+ showDirectoryPicker(): Promise<FileSystemDirectoryHandle>;
3
+ }
app/utils/constants.ts CHANGED
@@ -10,6 +10,8 @@ export const DEFAULT_PROVIDER = 'Anthropic';
10
  const staticModels: ModelInfo[] = [
11
  { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
12
  { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
 
 
13
  { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
14
  { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' },
15
  { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' },
 
10
  const staticModels: ModelInfo[] = [
11
  { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
12
  { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' },
13
+ { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', provider: 'OpenRouter' },
14
+ { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' },
15
  { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' },
16
  { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' },
17
  { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' },
package.json CHANGED
@@ -3,7 +3,6 @@
3
  "description": "StackBlitz AI Agent",
4
  "private": true,
5
  "license": "MIT",
6
- "packageManager": "[email protected]",
7
  "sideEffects": false,
8
  "type": "module",
9
  "scripts": {
@@ -96,6 +95,7 @@
96
  "is-ci": "^3.0.1",
97
  "node-fetch": "^3.3.2",
98
  "prettier": "^3.3.2",
 
99
  "typescript": "^5.5.2",
100
  "unified": "^11.0.5",
101
  "unocss": "^0.61.3",
 
3
  "description": "StackBlitz AI Agent",
4
  "private": true,
5
  "license": "MIT",
 
6
  "sideEffects": false,
7
  "type": "module",
8
  "scripts": {
 
95
  "is-ci": "^3.0.1",
96
  "node-fetch": "^3.3.2",
97
  "prettier": "^3.3.2",
98
+ "sass-embedded": "^1.80.3",
99
  "typescript": "^5.5.2",
100
  "unified": "^11.0.5",
101
  "unocss": "^0.61.3",
vite.config.ts CHANGED
@@ -27,6 +27,13 @@ export default defineConfig((config) => {
27
  chrome129IssuePlugin(),
28
  config.mode === 'production' && optimizeCssModules({ apply: 'build' }),
29
  ],
 
 
 
 
 
 
 
30
  };
31
  });
32
 
 
27
  chrome129IssuePlugin(),
28
  config.mode === 'production' && optimizeCssModules({ apply: 'build' }),
29
  ],
30
+ css: {
31
+ preprocessorOptions: {
32
+ scss: {
33
+ api: 'modern-compiler',
34
+ },
35
+ },
36
+ },
37
  };
38
  });
39