muzafferkadir commited on
Commit
49217f2
·
1 Parent(s): 50a501e

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.

Browse files
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={() => {
@@ -184,7 +205,6 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
184
  )
185
  );
186
  });
187
-
188
  interface ViewProps extends HTMLMotionProps<'div'> {
189
  children: JSX.Element;
190
  }
 
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={() => {
 
205
  )
206
  );
207
  });
 
208
  interface ViewProps extends HTMLMotionProps<'div'> {
209
  children: JSX.Element;
210
  }
app/lib/stores/workbench.ts CHANGED
@@ -280,21 +280,22 @@ export class WorkbenchStore {
280
 
281
  for (const [filePath, dirent] of Object.entries(files)) {
282
  if (dirent?.type === 'file' && !dirent.isBinary) {
283
- // Remove '/home/project/' from the beginning of the path
284
  const relativePath = filePath.replace(/^\/home\/project\//, '');
285
 
286
- // Split the path into segments
287
  const pathSegments = relativePath.split('/');
288
 
289
- // If there's more than one segment, we need to create folders
290
  if (pathSegments.length > 1) {
291
  let currentFolder = zip;
 
292
  for (let i = 0; i < pathSegments.length - 1; i++) {
293
  currentFolder = currentFolder.folder(pathSegments[i])!;
294
  }
295
  currentFolder.file(pathSegments[pathSegments.length - 1], dirent.content);
296
  } else {
297
- // If there's only one segment, it's a file in the root
298
  zip.file(relativePath, dirent.content);
299
  }
300
  }
@@ -303,6 +304,35 @@ export class WorkbenchStore {
303
  const content = await zip.generateAsync({ type: 'blob' });
304
  saveAs(content, 'project.zip');
305
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  }
307
 
308
  export const workbenchStore = new WorkbenchStore();
 
280
 
281
  for (const [filePath, dirent] of Object.entries(files)) {
282
  if (dirent?.type === 'file' && !dirent.isBinary) {
283
+ // remove '/home/project/' from the beginning of the path
284
  const relativePath = filePath.replace(/^\/home\/project\//, '');
285
 
286
+ // split the path into segments
287
  const pathSegments = relativePath.split('/');
288
 
289
+ // if there's more than one segment, we need to create folders
290
  if (pathSegments.length > 1) {
291
  let currentFolder = zip;
292
+
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
  }
 
304
  const content = await zip.generateAsync({ type: 'blob' });
305
  saveAs(content, 'project.zip');
306
  }
307
+
308
+ async syncFiles(targetHandle: FileSystemDirectoryHandle) {
309
+ const files = this.files.get();
310
+ const syncedFiles = [];
311
+
312
+ for (const [filePath, dirent] of Object.entries(files)) {
313
+ if (dirent?.type === 'file' && !dirent.isBinary) {
314
+ const relativePath = filePath.replace(/^\/home\/project\//, '');
315
+ const pathSegments = relativePath.split('/');
316
+ let currentHandle = targetHandle;
317
+
318
+ for (let i = 0; i < pathSegments.length - 1; i++) {
319
+ currentHandle = await currentHandle.getDirectoryHandle(pathSegments[i], { create: true });
320
+ }
321
+
322
+ // create or get the file
323
+ const fileHandle = await currentHandle.getFileHandle(pathSegments[pathSegments.length - 1], { create: true });
324
+
325
+ // write the file content
326
+ const writable = await fileHandle.createWritable();
327
+ await writable.write(dirent.content);
328
+ await writable.close();
329
+
330
+ syncedFiles.push(relativePath);
331
+ }
332
+ }
333
+
334
+ return syncedFiles;
335
+ }
336
  }
337
 
338
  export const workbenchStore = new WorkbenchStore();
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": {
@@ -94,6 +93,7 @@
94
  "is-ci": "^3.0.1",
95
  "node-fetch": "^3.3.2",
96
  "prettier": "^3.3.2",
 
97
  "typescript": "^5.5.2",
98
  "unified": "^11.0.5",
99
  "unocss": "^0.61.3",
 
3
  "description": "StackBlitz AI Agent",
4
  "private": true,
5
  "license": "MIT",
 
6
  "sideEffects": false,
7
  "type": "module",
8
  "scripts": {
 
93
  "is-ci": "^3.0.1",
94
  "node-fetch": "^3.3.2",
95
  "prettier": "^3.3.2",
96
+ "sass-embedded": "^1.80.3",
97
  "typescript": "^5.5.2",
98
  "unified": "^11.0.5",
99
  "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