diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 06066e2..e0eb41c 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -3,6 +3,8 @@ import React, { type RefCallback } from 'react'; import { ClientOnly } from 'remix-utils/client-only'; import styles from './BaseChat.module.scss'; import FilePreview from './FilePreview'; +import GitCloneButton from './GitCloneButton'; +import { ImportFolderButton } from './ImportFolderButton'; import { Messages } from './Messages.client'; import { SendButton } from './SendButton.client'; import StarterTemplates from './StarterTemplates'; @@ -10,8 +12,6 @@ import { Menu } from '~/components/sidebar/Menu.client'; import { IconButton } from '~/components/ui/IconButton'; import { Workbench } from '~/components/workbench/Workbench.client'; import { classNames } from '~/utils/classNames'; -import GitCloneButton from './GitCloneButton'; -import { ImportFolderButton } from './ImportFolderButton'; interface BaseChatProps { textareaRef?: React.RefObject | undefined; @@ -226,18 +226,22 @@ export const BaseChat = React.forwardRef( {!chatStarted && (
- { - sendMessage?.(new Event('click') as any, description); - messages.forEach((message) => { - sendMessage?.(new Event('click') as any, message.content); - }); - }} /> - { - sendMessage?.(new Event('click') as any, description); - messages.forEach((message) => { - sendMessage?.(new Event('click') as any, message.content); - }); - }} /> + { + sendMessage?.(new Event('click') as any, description); + messages.forEach((message) => { + sendMessage?.(new Event('click') as any, message.content); + }); + }} + /> + { + sendMessage?.(new Event('click') as any, description); + messages.forEach((message) => { + sendMessage?.(new Event('click') as any, message.content); + }); + }} + />
{EXAMPLE_PROMPTS.map((examplePrompt, index) => { diff --git a/app/components/chat/GitCloneButton.tsx b/app/components/chat/GitCloneButton.tsx index 40f7f5b..2a20199 100644 --- a/app/components/chat/GitCloneButton.tsx +++ b/app/components/chat/GitCloneButton.tsx @@ -1,8 +1,8 @@ -import ignore from 'ignore'; -import { useGit } from '~/lib/hooks/useGit'; import type { Message } from 'ai'; +import ignore from 'ignore'; import WithTooltip from '~/components/ui/Tooltip'; import { IGNORE_PATTERNS } from '~/constants/ignorePatterns'; +import { useGit } from '~/lib/hooks/useGit'; const ig = ignore().add(IGNORE_PATTERNS); const generateId = () => Math.random().toString(36).substring(2, 15); @@ -75,4 +75,4 @@ ${textDecoder.decode(content)} ); -} \ No newline at end of file +} diff --git a/app/components/chat/ImportFolderButton.tsx b/app/components/chat/ImportFolderButton.tsx index 56e3aa1..67c3bda 100644 --- a/app/components/chat/ImportFolderButton.tsx +++ b/app/components/chat/ImportFolderButton.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import type { Message } from 'ai'; -import { toast } from 'react-toastify'; import ignore from 'ignore'; +import React from 'react'; +import { toast } from 'react-toastify'; import WithTooltip from '~/components/ui/Tooltip'; import { IGNORE_PATTERNS } from '~/constants/ignorePatterns'; @@ -14,14 +14,14 @@ const ig = ignore().add(IGNORE_PATTERNS); const generateId = () => Math.random().toString(36).substring(2, 15); const isBinaryFile = async (file: File): Promise => { - const chunkSize = 1024; // Read the first 1 KB of the file + const chunkSize = 1024; // read the first 1 KB of the file const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer()); for (let i = 0; i < buffer.length; i++) { const byte = buffer[i]; if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) { - return true; // Found a binary character + return true; // found a binary character } } @@ -130,9 +130,9 @@ ${fileArtifacts.join('\n\n')} toast.error('Failed to import folder'); } - e.target.value = ''; // Reset file input + e.target.value = ''; // reset file input }} - {...({} as any)} // if removed webkitdirectory will throw errors as unknow attribute + {...({} as any)} // if removed, webkitdirectory will throw errors as unknown attribute />
); -}; \ No newline at end of file +}; diff --git a/app/components/chat/StarterTemplates.tsx b/app/components/chat/StarterTemplates.tsx index bac7424..700cd03 100644 --- a/app/components/chat/StarterTemplates.tsx +++ b/app/components/chat/StarterTemplates.tsx @@ -14,11 +14,7 @@ const FrameworkLink = memo(({ template }) => ( className="items-center justify-center" target="_self" > - {template.label} + {template.label} )); @@ -37,4 +33,4 @@ const StarterTemplates = memo(() => { ); }); -export default StarterTemplates; \ No newline at end of file +export default StarterTemplates; diff --git a/app/components/git/GitUrlImport.client.tsx b/app/components/git/GitUrlImport.client.tsx index 337d861..5a2ab63 100644 --- a/app/components/git/GitUrlImport.client.tsx +++ b/app/components/git/GitUrlImport.client.tsx @@ -61,7 +61,7 @@ export function GitUrlImport({ initialUrl }: GitUrlImportProps) { const textDecoder = new TextDecoder('utf-8'); - // Convert files to common format for command detection + // convert files to common format for command detection const fileContents = filePaths .map((filePath) => { const { data: content, encoding } = data[filePath]; @@ -72,11 +72,11 @@ export function GitUrlImport({ initialUrl }: GitUrlImportProps) { }) .filter((f) => f.content); - // Detect and create commands message + // detect and create commands message const commands = await detectProjectCommands(fileContents); const commandsMessage = createCommandsMessage(commands); - // Create files message + // create files message const filesMessage: Message = { role: 'assistant', content: `Cloning the repo ${repoUrl} into ${workdir} @@ -101,9 +101,10 @@ ${file.content} } await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); - - // Wait for the chat ID to be set + + // wait for the chat ID to be set const id = chatId.get(); + if (id) { navigateChat(id); } @@ -116,7 +117,7 @@ ${file.content} return; } - // Use initialUrl if provided, otherwise fallback to URL parameter + // use initialUrl if provided, otherwise fallback to URL parameter const url = initialUrl || searchParams.get('url'); if (!url) { @@ -129,4 +130,4 @@ ${file.content} }, [searchParams, historyReady, gitReady, imported, initialUrl]); return }>{() => }; -} \ No newline at end of file +} diff --git a/app/components/ui/Tooltip.tsx b/app/components/ui/Tooltip.tsx index 115f198..6af2716 100644 --- a/app/components/ui/Tooltip.tsx +++ b/app/components/ui/Tooltip.tsx @@ -6,7 +6,7 @@ interface WithTooltipProps { children: React.ReactNode; } -export default memo(function WithTooltip({ tooltip, children }: WithTooltipProps) { +export default memo(({ tooltip, children }: WithTooltipProps) => { return ( @@ -23,4 +23,4 @@ export default memo(function WithTooltip({ tooltip, children }: WithTooltipProps ); -}); \ No newline at end of file +}); diff --git a/app/components/workbench/GitHubPushModal.tsx b/app/components/workbench/GitHubPushModal.tsx index 9e2fadd..6758739 100644 --- a/app/components/workbench/GitHubPushModal.tsx +++ b/app/components/workbench/GitHubPushModal.tsx @@ -1,4 +1,3 @@ -import { useStore } from '@nanostores/react'; import { useState } from 'react'; import { toast } from 'react-toastify'; import { githubStore } from '~/lib/stores/github'; @@ -29,7 +28,9 @@ export function GitHubPushModal({ isOpen, onClose }: GitHubPushModalProps) { } }; - if (!isOpen) return null; + if (!isOpen) { + return null; + } return (
@@ -97,4 +98,4 @@ export function GitHubPushModal({ isOpen, onClose }: GitHubPushModalProps) {
); -} \ No newline at end of file +} diff --git a/app/components/workbench/Preview.tsx b/app/components/workbench/Preview.tsx index 0be0207..abf44e5 100644 --- a/app/components/workbench/Preview.tsx +++ b/app/components/workbench/Preview.tsx @@ -82,26 +82,32 @@ export const Preview = memo(() => { icon="i-ph:arrow-square-out" onClick={() => { console.log('Button clicked'); + if (activePreview?.baseUrl) { console.log('Active preview baseUrl:', activePreview.baseUrl); - // Extract the preview ID from the WebContainer URL + + // extract the preview ID from the WebContainer URL const match = activePreview.baseUrl.match( /^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/, ); console.log('URL match:', match); + if (match) { const previewId = match[1]; console.log('Preview ID:', previewId); - // Open in new tab using our route with absolute path - // Use the current port for development + /** + * Open in new tab using our route with absolute path. + * Use the current port for development. + */ const port = window.location.port ? `:${window.location.port}` : ''; const previewUrl = `${window.location.protocol}//${window.location.hostname}${port}/webcontainer/preview/${previewId}`; console.log('Opening URL:', previewUrl); + const newWindow = window.open(previewUrl, '_blank', 'noopener,noreferrer'); - // Force focus on the new window + // force focus on the new window if (newWindow) { newWindow.focus(); } else { diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 04962ad..8c13d7d 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -3,7 +3,8 @@ import { saveAs } from 'file-saver'; import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'; import JSZip from 'jszip'; import { computed } from 'nanostores'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import React from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { EditorPanel } from './EditorPanel'; import { GitHubPushModal } from './GitHubPushModal'; @@ -66,9 +67,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => const files = useStore(workbenchStore.files); const selectedView = useStore(workbenchStore.currentView); const [showGitHubModal, setShowGitHubModal] = useState(false); - const [showGitHubPushModal, setShowGitHubPushModal] = useState(false); const [isSyncing, setIsSyncing] = useState(false); - const fileInputRef = useRef(null); const handleSyncFiles = useCallback(async () => { setIsSyncing(true); @@ -79,9 +78,11 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => await workbenchStore.syncFiles(directoryHandle); toast.success('Files synced successfully'); } else { - // Fallback to download as zip + // fallback to download as zip await downloadZip(); - toast.info('Your browser does not support the File System Access API. Files have been downloaded as a zip instead.'); + toast.info( + 'Your browser does not support the File System Access API. Files have been downloaded as a zip instead.', + ); } } catch (error) { console.error('Error syncing files:', error); @@ -189,10 +190,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
Download - setShowGitHubModal(true)} - > + setShowGitHubModal(true)}>
Push to GitHub @@ -244,17 +242,14 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
- setShowGitHubModal(false)} - /> + setShowGitHubModal(false)} /> ) ); }); interface ViewProps extends HTMLMotionProps<'div'> { - children: JSX.Element; + children: React.ReactElement; } const View = memo(({ children, ...props }: ViewProps) => { diff --git a/app/constants/ignorePatterns.ts b/app/constants/ignorePatterns.ts index c8e3693..34fd230 100644 --- a/app/constants/ignorePatterns.ts +++ b/app/constants/ignorePatterns.ts @@ -20,8 +20,9 @@ export const IGNORE_PATTERNS = [ '**/yarn-error.log*', '**/*lock.json', '**/*lock.yaml', - // Binary files + + // binary files '**/*.jpg', '**/*.jpeg', '**/*.png', -]; \ No newline at end of file +]; diff --git a/app/lib/hooks/useGit.ts b/app/lib/hooks/useGit.ts index 1b55282..ffb0e65 100644 --- a/app/lib/hooks/useGit.ts +++ b/app/lib/hooks/useGit.ts @@ -1,10 +1,10 @@ import type { WebContainer } from '@webcontainer/api'; -import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react'; -import { webcontainer as webcontainerPromise } from '~/lib/webcontainer'; import git, { type GitAuth, type PromiseFsClient } from 'isomorphic-git'; import http from 'isomorphic-git/http/web'; import Cookies from 'js-cookie'; +import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react'; import { toast } from 'react-toastify'; +import { webcontainer as webcontainerPromise } from '~/lib/webcontainer'; const lookupSavedPassword = (url: string) => { const domain = url.split('/')[2]; @@ -144,13 +144,16 @@ const getFs = ( return await webcontainer.fs.rm(relativePath, { recursive: true, ...options }); }, - // Mock implementations for missing functions + // mock implementations for missing functions unlink: async (path: string) => { // unlink is just removing a single file const relativePath = pathUtils.relative(webcontainer.workdir, path); return await webcontainer.fs.rm(relativePath, { recursive: false }); }, + /** + * Get file or directory information. + */ stat: async (path: string) => { try { const relativePath = pathUtils.relative(webcontainer.workdir, path); @@ -167,7 +170,7 @@ const getFs = ( isDirectory: () => fileInfo.isDirectory(), isSymbolicLink: () => false, size: 1, - mode: 0o666, // Default permissions + mode: 0o666, // default permissions mtimeMs: Date.now(), uid: 1000, gid: 1000, @@ -184,35 +187,35 @@ const getFs = ( } }, + /** + * For basic usage, lstat can return the same as stat + * since we're not handling symbolic links. + */ lstat: async (path: string) => { - /* - * For basic usage, lstat can return the same as stat - * since we're not handling symbolic links - */ return await getFs(webcontainer, record).promises.stat(path); }, + /** + * Since WebContainer doesn't support symlinks, + * we'll throw a "not a symbolic link" error. + */ readlink: async (path: string) => { - /* - * Since WebContainer doesn't support symlinks, - * we'll throw a "not a symbolic link" error - */ throw new Error(`EINVAL: invalid argument, readlink '${path}'`); }, + /** + * Since WebContainer doesn't support symlinks, + * we'll throw a "operation not supported" error. + */ symlink: async (target: string, path: string) => { - /* - * Since WebContainer doesn't support symlinks, - * we'll throw a "operation not supported" error - */ throw new Error(`EPERM: operation not permitted, symlink '${target}' -> '${path}'`); }, + /** + * WebContainer doesn't support changing permissions, + * but we can pretend it succeeded for compatibility. + */ chmod: async (_path: string, _mode: number) => { - /* - * WebContainer doesn't support changing permissions, - * but we can pretend it succeeded for compatibility - */ return await Promise.resolve(); }, }, @@ -220,26 +223,26 @@ const getFs = ( const pathUtils = { dirname: (path: string) => { - // Handle empty or just filename cases + // handle empty or just filename cases if (!path || !path.includes('/')) { return '.'; } - // Remove trailing slashes + // remove trailing slashes path = path.replace(/\/+$/, ''); - // Get directory part + // get directory part return path.split('/').slice(0, -1).join('/') || '/'; }, basename: (path: string, ext?: string) => { - // Remove trailing slashes + // remove trailing slashes path = path.replace(/\/+$/, ''); - // Get the last part of the path + // get the last part of the path const base = path.split('/').pop() || ''; - // If extension is provided, remove it from the result + // if extension is provided, remove it from the result if (ext && base.endsWith(ext)) { return base.slice(0, -ext.length); } @@ -247,18 +250,18 @@ const pathUtils = { return base; }, relative: (from: string, to: string): string => { - // Handle empty inputs + // handle empty inputs if (!from || !to) { return '.'; } - // Normalize paths by removing trailing slashes and splitting + // normalize paths by removing trailing slashes and splitting const normalizePathParts = (p: string) => p.replace(/\/+$/, '').split('/').filter(Boolean); const fromParts = normalizePathParts(from); const toParts = normalizePathParts(to); - // Find common parts at the start of both paths + // find common parts at the start of both paths let commonLength = 0; const minLength = Math.min(fromParts.length, toParts.length); @@ -270,16 +273,16 @@ const pathUtils = { commonLength++; } - // Calculate the number of "../" needed + // calculate the number of "../" needed const upCount = fromParts.length - commonLength; - // Get the remaining path parts we need to append + // get the remaining path parts we need to append const remainingPath = toParts.slice(commonLength); - // Construct the relative path + // construct the relative path const relativeParts = [...Array(upCount).fill('..'), ...remainingPath]; - // Handle empty result case + // handle empty result case return relativeParts.length === 0 ? '.' : relativeParts.join('/'); }, -}; \ No newline at end of file +}; diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index 935f616..cf03a05 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -32,42 +32,48 @@ export function useChatHistory() { const [ready, setReady] = useState(false); const [urlId, setUrlId] = useState(); - const storeMessageHistory = useCallback(async (messages: Message[]) => { - if (!db || messages.length === 0) { - return; - } - - const { firstArtifact } = workbenchStore; - - if (!urlId && firstArtifact?.id) { - const urlId = await getUrlId(db, firstArtifact.id); - - navigateChat(urlId); - setUrlId(urlId); - } - - if (!description.get() && firstArtifact?.title) { - description.set(firstArtifact?.title); - } - - if (initialMessages.length === 0 && !chatId.get()) { - const nextId = await getNextId(db); - - chatId.set(nextId); - - if (!urlId) { - navigateChat(nextId); + const storeMessageHistory = useCallback( + async (messages: Message[]) => { + if (!db || messages.length === 0) { + return; } - } - await setMessages(db, chatId.get() as string, messages, urlId, description.get()); - }, [initialMessages.length, urlId]); + const { firstArtifact } = workbenchStore; - const importChat = useCallback(async (chatDescription: string, messages: Message[]) => { - logger.trace('Importing chat', { description: chatDescription, messages }); - description.set(chatDescription); - await storeMessageHistory(messages); - }, [storeMessageHistory]); + if (!urlId && firstArtifact?.id) { + const urlId = await getUrlId(db, firstArtifact.id); + + navigateChat(urlId); + setUrlId(urlId); + } + + if (!description.get() && firstArtifact?.title) { + description.set(firstArtifact?.title); + } + + if (initialMessages.length === 0 && !chatId.get()) { + const nextId = await getNextId(db); + + chatId.set(nextId); + + if (!urlId) { + navigateChat(nextId); + } + } + + await setMessages(db, chatId.get() as string, messages, urlId, description.get()); + }, + [initialMessages.length, urlId], + ); + + const importChat = useCallback( + async (chatDescription: string, messages: Message[]) => { + logger.trace('Importing chat', { description: chatDescription, messages }); + description.set(chatDescription); + await storeMessageHistory(messages); + }, + [storeMessageHistory], + ); useEffect(() => { if (!db) { @@ -104,7 +110,7 @@ export function useChatHistory() { ready: !mixedId || ready, initialMessages, storeMessageHistory, - importChat + importChat, }; } diff --git a/app/lib/stores/github.ts b/app/lib/stores/github.ts index b334d12..cd73c5a 100644 --- a/app/lib/stores/github.ts +++ b/app/lib/stores/github.ts @@ -12,12 +12,12 @@ class GitHubStore { async pushToGitHub(token: string, username: string, repoName: string): Promise { try { - // First, create the repository if it doesn't exist + // first, create the repository if it doesn't exist const createRepoResponse = await fetch(`https://api.github.com/user/repos`, { method: 'POST', headers: { - 'Authorization': `token ${token}`, - 'Accept': 'application/vnd.github.v3+json', + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -27,31 +27,35 @@ class GitHubStore { }), }); - if (!createRepoResponse.ok && createRepoResponse.status !== 422) { // 422 means repo already exists + if (!createRepoResponse.ok && createRepoResponse.status !== 422) { + // 422 means repo already exists throw new Error(`Failed to create repository: ${createRepoResponse.statusText}`); } - // Get all files from workbench + // get all files from workbench const files = workbenchStore.files.get(); - - // Create a commit with all files + + // create a commit with all files for (const [filePath, dirent] of Object.entries(files)) { if (dirent?.type === 'file' && !dirent.isBinary) { const relativePath = filePath.replace(/^\/home\/project\//, ''); - - // Create/update file in repository - const response = await fetch(`https://api.github.com/repos/${username}/${repoName}/contents/${relativePath}`, { - method: 'PUT', - headers: { - 'Authorization': `token ${token}`, - 'Accept': 'application/vnd.github.v3+json', - 'Content-Type': 'application/json', + + // create/update file in repository + const response = await fetch( + `https://api.github.com/repos/${username}/${repoName}/contents/${relativePath}`, + { + method: 'PUT', + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: `Add/Update ${relativePath}`, + content: btoa(dirent.content), // base64 encode content + }), }, - body: JSON.stringify({ - message: `Add/Update ${relativePath}`, - content: btoa(dirent.content), // Base64 encode content - }), - }); + ); if (!response.ok) { throw new Error(`Failed to push file ${relativePath}: ${response.statusText}`); @@ -59,9 +63,8 @@ class GitHubStore { } } - // Update config store + // update config store this.config.set({ token, username, repoName }); - } catch (error) { console.error('Error pushing to GitHub:', error); throw error; @@ -69,4 +72,4 @@ class GitHubStore { } } -export const githubStore = new GitHubStore(); \ No newline at end of file +export const githubStore = new GitHubStore(); diff --git a/app/lib/stores/previews.ts b/app/lib/stores/previews.ts index a79ccbc..811c4c0 100644 --- a/app/lib/stores/previews.ts +++ b/app/lib/stores/previews.ts @@ -2,7 +2,7 @@ import type { WebContainer, PathWatcherEvent } from '@webcontainer/api'; import { atom } from 'nanostores'; import { bufferWatchEvents } from '~/utils/buffer'; -// Extend Window interface to include our custom property +// extend window interface to include our custom property declare global { interface Window { _tabId?: string; @@ -15,7 +15,7 @@ export interface PreviewInfo { baseUrl: string; } -// Create a broadcast channel for preview updates +// create a broadcast channel for preview updates const PREVIEW_CHANNEL = 'preview-updates'; export class PreviewsStore { @@ -35,7 +35,7 @@ export class PreviewsStore { this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL); this.#storageChannel = new BroadcastChannel('storage-sync-channel'); - // Listen for preview updates from other tabs + // listen for preview updates from other tabs this.#broadcastChannel.onmessage = (event) => { const { type, previewId } = event.data; @@ -50,7 +50,7 @@ export class PreviewsStore { } }; - // Listen for storage sync messages + // listen for storage sync messages this.#storageChannel.onmessage = (event) => { const { storage, source } = event.data; @@ -59,7 +59,7 @@ export class PreviewsStore { } }; - // Override localStorage setItem to catch all changes + // override localStorage setItem to catch all changes if (typeof window !== 'undefined') { const originalSetItem = localStorage.setItem; @@ -72,7 +72,7 @@ export class PreviewsStore { this.#init(); } - // Generate a unique ID for this tab + // generate a unique ID for this tab private _getTabId(): string { if (typeof window !== 'undefined') { if (!window._tabId) { @@ -85,7 +85,7 @@ export class PreviewsStore { return ''; } - // Sync storage data between tabs + // sync storage data between tabs private _syncStorage(storage: Record) { if (typeof window !== 'undefined') { Object.entries(storage).forEach(([key, value]) => { @@ -97,7 +97,7 @@ export class PreviewsStore { } }); - // Force a refresh after syncing storage + // force a refresh after syncing storage const previews = this.previews.get(); previews.forEach((preview) => { const previewId = this.getPreviewId(preview.baseUrl); @@ -109,7 +109,7 @@ export class PreviewsStore { } } - // Broadcast storage state to other tabs + // broadcast storage state to other tabs private _broadcastStorageSync() { if (typeof window !== 'undefined') { const storage: Record = {}; @@ -134,21 +134,20 @@ export class PreviewsStore { async #init() { const webcontainer = await this.#webcontainer; - // Listen for server ready events + // listen for server ready events webcontainer.on('server-ready', (port, url) => { console.log('[Preview] Server ready on port:', port, url); this.broadcastUpdate(url); - // Initial storage sync when preview is ready + // initial storage sync when preview is ready this._broadcastStorageSync(); }); try { - // Watch for file changes + // watch for file changes webcontainer.internal.watchPaths( { include: ['**/*'], exclude: ['**/node_modules', '.git'], includeContent: true }, - bufferWatchEvents<[PathWatcherEvent[]]>(100, (events) => { - const watchEvents = events.flat(2); + bufferWatchEvents<[PathWatcherEvent[]]>(100, (_events) => { const previews = this.previews.get(); for (const preview of previews) { @@ -161,10 +160,10 @@ export class PreviewsStore { }), ); - // Watch for DOM changes that might affect storage + // watch for DOM changes that might affect storage if (typeof window !== 'undefined') { const observer = new MutationObserver(() => { - // Broadcast storage changes when DOM changes + // broadcast storage changes when DOM changes this._broadcastStorageSync(); }); @@ -179,7 +178,7 @@ export class PreviewsStore { console.error('[Preview] Error setting up watchers:', error); } - // Listen for port events + // listen for port events webcontainer.on('port', (port, type, url) => { let previewInfo = this.#availablePreviews.get(port); const previews = this.previews.get(); @@ -206,13 +205,13 @@ export class PreviewsStore { }); } - // Helper to extract preview ID from URL + // helper to extract preview ID from URL getPreviewId(url: string): string | null { const match = url.match(/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/); return match ? match[1] : null; } - // Broadcast state change to all tabs + // broadcast state change to all tabs broadcastStateChange(previewId: string) { const timestamp = Date.now(); this.#lastUpdate.set(previewId, timestamp); @@ -224,7 +223,7 @@ export class PreviewsStore { }); } - // Broadcast file change to all tabs + // broadcast file change to all tabs broadcastFileChange(previewId: string) { const timestamp = Date.now(); this.#lastUpdate.set(previewId, timestamp); @@ -236,7 +235,7 @@ export class PreviewsStore { }); } - // Broadcast update to all tabs + // broadcast update to all tabs broadcastUpdate(url: string) { const previewId = this.getPreviewId(url); @@ -252,16 +251,16 @@ export class PreviewsStore { } } - // Method to refresh a specific preview + // method to refresh a specific preview refreshPreview(previewId: string) { - // Clear any pending refresh for this preview + // clear any pending refresh for this preview const existingTimeout = this.#refreshTimeouts.get(previewId); if (existingTimeout) { clearTimeout(existingTimeout); } - // Set a new timeout for this refresh + // set a new timeout for this refresh const timeout = setTimeout(() => { const previews = this.previews.get(); const preview = previews.find((p) => this.getPreviewId(p.baseUrl) === previewId); diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index 0eb1da3..b0a5f4f 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -257,7 +257,11 @@ export class WorkbenchStore { async runAction(data: ActionCallbackData) { const artifact = this.artifacts.get()[data.messageId]; - if (!artifact) return; + + if (!artifact) { + return; + } + await artifact.runner.runAction(data); } diff --git a/app/lib/webcontainer/index.ts b/app/lib/webcontainer/index.ts index 7af2132..02f2453 100644 --- a/app/lib/webcontainer/index.ts +++ b/app/lib/webcontainer/index.ts @@ -25,7 +25,7 @@ if (!import.meta.env.SSR) { return WebContainer.boot({ coep: 'credentialless', workdirName: WORK_DIR_NAME, - forwardPreviewErrors: true, // Enable error forwarding from iframes + forwardPreviewErrors: true, // enable error forwarding from iframes }); }) .then((webcontainer) => { diff --git a/app/routes/git.tsx b/app/routes/git.tsx index 2bad25c..0ef9811 100644 --- a/app/routes/git.tsx +++ b/app/routes/git.tsx @@ -17,7 +17,7 @@ interface LoaderData { export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const gitUrl = url.searchParams.get('url'); - + if (!gitUrl) { throw new Response('No Git URL provided', { status: 400 }); } @@ -27,11 +27,11 @@ export async function loader({ request }: LoaderFunctionArgs) { export default function Index() { const data = useLoaderData(); - + return (
}>{() => }
); -} \ No newline at end of file +} diff --git a/app/routes/webcontainer.preview.$id.tsx b/app/routes/webcontainer.preview.$id.tsx index a5f00b4..c693c25 100644 --- a/app/routes/webcontainer.preview.$id.tsx +++ b/app/routes/webcontainer.preview.$id.tsx @@ -20,10 +20,10 @@ export default function WebContainerPreview() { const broadcastChannelRef = useRef(); const [previewUrl, setPreviewUrl] = useState(''); - // Handle preview refresh + // handle preview refresh const handleRefresh = useCallback(() => { if (iframeRef.current && previewUrl) { - // Force a clean reload + // force a clean reload iframeRef.current.src = ''; requestAnimationFrame(() => { if (iframeRef.current) { @@ -33,7 +33,7 @@ export default function WebContainerPreview() { } }, [previewUrl]); - // Notify other tabs that this preview is ready + // notify other tabs that this preview is ready const notifyPreviewReady = useCallback(() => { if (broadcastChannelRef.current && previewUrl) { broadcastChannelRef.current.postMessage({ @@ -46,10 +46,10 @@ export default function WebContainerPreview() { }, [previewId, previewUrl]); useEffect(() => { - // Initialize broadcast channel + // initialize broadcast channel broadcastChannelRef.current = new BroadcastChannel(PREVIEW_CHANNEL); - // Listen for preview updates + // listen for preview updates broadcastChannelRef.current.onmessage = (event) => { if (event.data.previewId === previewId) { if (event.data.type === 'refresh-preview' || event.data.type === 'file-change') { @@ -58,19 +58,19 @@ export default function WebContainerPreview() { } }; - // Construct the WebContainer preview URL + // construct the WebContainer preview URL const url = `https://${previewId}.local-credentialless.webcontainer-api.io`; setPreviewUrl(url); - // Set the iframe src + // set the iframe src if (iframeRef.current) { iframeRef.current.src = url; } - // Notify other tabs that this preview is ready + // notify other tabs that this preview is ready notifyPreviewReady(); - // Cleanup + // cleanup return () => { broadcastChannelRef.current?.close(); }; @@ -89,4 +89,4 @@ export default function WebContainerPreview() { /> ); -} \ No newline at end of file +} diff --git a/app/types/global.d.ts b/app/types/global.d.ts index 6445f3a..cad0021 100644 --- a/app/types/global.d.ts +++ b/app/types/global.d.ts @@ -14,4 +14,4 @@ interface FileSystemFileHandle { interface FileSystemWritableFileStream extends WritableStream { write(data: string | BufferSource | Blob): Promise; close(): Promise; -} \ No newline at end of file +} diff --git a/app/types/template.ts b/app/types/template.ts index 93928c7..2194c7b 100644 --- a/app/types/template.ts +++ b/app/types/template.ts @@ -5,4 +5,4 @@ export interface Template { githubRepo: string; tags?: string[]; icon?: string; -} \ No newline at end of file +} diff --git a/app/utils/projectCommands.ts b/app/utils/projectCommands.ts index cffabb9..d0aaa87 100644 --- a/app/utils/projectCommands.ts +++ b/app/utils/projectCommands.ts @@ -13,35 +13,36 @@ interface DetectedCommand { export async function detectProjectCommands(files: FileContent[]): Promise { const commands: DetectedCommand[] = []; - - // Look for package.json to detect npm/node projects - const packageJson = files.find(f => f.path === 'package.json'); + + // look for package.json to detect npm/node projects + const packageJson = files.find((f) => f.path === 'package.json'); + if (packageJson) { try { const pkg = JSON.parse(packageJson.content); - - // Add install command + + // add install command commands.push({ type: 'install', command: 'npm install', - description: 'Install dependencies' + description: 'Install dependencies', }); - // Add dev command if it exists + // add dev command if it exists if (pkg.scripts?.dev) { commands.push({ type: 'dev', command: 'npm run dev', - description: 'Start development server' + description: 'Start development server', }); } - // Add build command if it exists + // add build command if it exists if (pkg.scripts?.build) { commands.push({ type: 'build', command: 'npm run build', - description: 'Build the project' + description: 'Build the project', }); } } catch (e) { @@ -57,11 +58,14 @@ export function createCommandsMessage(commands: DetectedCommand[]): Message | nu return null; } - const commandsContent = commands.map(cmd => - ` + const commandsContent = commands + .map( + (cmd) => + ` ${cmd.command} -` - ).join('\n'); +`, + ) + .join('\n'); return { role: 'assistant', @@ -70,6 +74,6 @@ ${cmd.command} ${commandsContent} `, id: generateId(), - createdAt: new Date() + createdAt: new Date(), }; -} \ No newline at end of file +}