From 49217f246177a30bf99fdd2465beeafa014a92da Mon Sep 17 00:00:00 2001 From: muzafferkadir Date: Tue, 22 Oct 2024 01:27:29 +0300 Subject: [PATCH] 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 | 24 +++++++++++- app/lib/stores/workbench.ts | 38 +++++++++++++++++-- app/types/global.d.ts | 3 ++ app/utils/constants.ts | 2 + package.json | 2 +- vite.config.ts | 7 ++++ 6 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 app/types/global.d.ts diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 839e9a8..4dd700d 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -1,7 +1,7 @@ import { useStore } from '@nanostores/react'; import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'; import { computed } from 'nanostores'; -import { memo, useCallback, useEffect } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { type OnChangeCallback as OnEditorChange, @@ -55,6 +55,8 @@ const workbenchVariants = { export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => { renderLogger.trace('Workbench'); + const [isSyncing, setIsSyncing] = useState(false); + const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0)); const showWorkbench = useStore(workbenchStore.showWorkbench); const selectedFile = useStore(workbenchStore.selectedFile); @@ -99,6 +101,21 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => workbenchStore.resetCurrentDocument(); }, []); + const handleSyncFiles = useCallback(async () => { + setIsSyncing(true); + + try { + const directoryHandle = await window.showDirectoryPicker(); + await workbenchStore.syncFiles(directoryHandle); + toast.success('Files synced successfully'); + } catch (error) { + console.error('Error syncing files:', error); + toast.error('Failed to sync files'); + } finally { + setIsSyncing(false); + } + }, []); + return ( chatStarted && (
Download Code + + {isSyncing ?
:
} + {isSyncing ? 'Syncing...' : 'Sync Files'} + { @@ -184,7 +205,6 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => ) ); }); - interface ViewProps extends HTMLMotionProps<'div'> { children: JSX.Element; } diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index 4b85bc2..e9f726d 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -280,21 +280,22 @@ export class WorkbenchStore { for (const [filePath, dirent] of Object.entries(files)) { if (dirent?.type === 'file' && !dirent.isBinary) { - // Remove '/home/project/' from the beginning of the path + // remove '/home/project/' from the beginning of the path const relativePath = filePath.replace(/^\/home\/project\//, ''); - // Split the path into segments + // split the path into segments const pathSegments = relativePath.split('/'); - // If there's more than one segment, we need to create folders + // if there's more than one segment, we need to create folders if (pathSegments.length > 1) { let currentFolder = zip; + for (let i = 0; i < pathSegments.length - 1; i++) { currentFolder = currentFolder.folder(pathSegments[i])!; } currentFolder.file(pathSegments[pathSegments.length - 1], dirent.content); } else { - // If there's only one segment, it's a file in the root + // if there's only one segment, it's a file in the root zip.file(relativePath, dirent.content); } } @@ -303,6 +304,35 @@ export class WorkbenchStore { const content = await zip.generateAsync({ type: 'blob' }); saveAs(content, 'project.zip'); } + + async syncFiles(targetHandle: FileSystemDirectoryHandle) { + const files = this.files.get(); + const syncedFiles = []; + + for (const [filePath, dirent] of Object.entries(files)) { + if (dirent?.type === 'file' && !dirent.isBinary) { + const relativePath = filePath.replace(/^\/home\/project\//, ''); + const pathSegments = relativePath.split('/'); + let currentHandle = targetHandle; + + for (let i = 0; i < pathSegments.length - 1; i++) { + currentHandle = await currentHandle.getDirectoryHandle(pathSegments[i], { create: true }); + } + + // create or get the file + const fileHandle = await currentHandle.getFileHandle(pathSegments[pathSegments.length - 1], { create: true }); + + // write the file content + const writable = await fileHandle.createWritable(); + await writable.write(dirent.content); + await writable.close(); + + syncedFiles.push(relativePath); + } + } + + return syncedFiles; + } } export const workbenchStore = new WorkbenchStore(); diff --git a/app/types/global.d.ts b/app/types/global.d.ts new file mode 100644 index 0000000..a1f6789 --- /dev/null +++ b/app/types/global.d.ts @@ -0,0 +1,3 @@ +interface Window { + showDirectoryPicker(): Promise; +} diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 6db860c..eb7a6eb 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -10,6 +10,8 @@ export const DEFAULT_PROVIDER = 'Anthropic'; const staticModels: ModelInfo[] = [ { name: 'claude-3-5-sonnet-20240620', label: 'Claude 3.5 Sonnet', provider: 'Anthropic' }, { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' }, + { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', provider: 'OpenRouter' }, + { name: 'anthropic/claude-3-haiku', label: 'Anthropic: Claude 3 Haiku (OpenRouter)', provider: 'OpenRouter' }, { name: 'deepseek/deepseek-coder', label: 'Deepseek-Coder V2 236B (OpenRouter)', provider: 'OpenRouter' }, { name: 'google/gemini-flash-1.5', label: 'Google Gemini Flash 1.5 (OpenRouter)', provider: 'OpenRouter' }, { name: 'google/gemini-pro-1.5', label: 'Google Gemini Pro 1.5 (OpenRouter)', provider: 'OpenRouter' }, diff --git a/package.json b/package.json index 737ca05..2bd5173 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "description": "StackBlitz AI Agent", "private": true, "license": "MIT", - "packageManager": "pnpm@9.4.0", "sideEffects": false, "type": "module", "scripts": { @@ -94,6 +93,7 @@ "is-ci": "^3.0.1", "node-fetch": "^3.3.2", "prettier": "^3.3.2", + "sass-embedded": "^1.80.3", "typescript": "^5.5.2", "unified": "^11.0.5", "unocss": "^0.61.3", diff --git a/vite.config.ts b/vite.config.ts index 58e76cd..1c5a0a6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,6 +27,13 @@ export default defineConfig((config) => { chrome129IssuePlugin(), config.mode === 'production' && optimizeCssModules({ apply: 'build' }), ], + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, }; });