chore: lint fix

completed lint fixes
This commit is contained in:
Dustin Loring 2025-01-17 15:16:21 -05:00 committed by GitHub
commit d84ae2ab19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 233 additions and 210 deletions

View File

@ -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<HTMLTextAreaElement> | undefined;
@ -226,18 +226,22 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{!chatStarted && (
<div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex flex-col items-center">
<div className="flex gap-4">
<GitCloneButton importChat={async (description, messages) => {
sendMessage?.(new Event('click') as any, description);
messages.forEach((message) => {
sendMessage?.(new Event('click') as any, message.content);
});
}} />
<ImportFolderButton importChat={async (description, messages) => {
sendMessage?.(new Event('click') as any, description);
messages.forEach((message) => {
sendMessage?.(new Event('click') as any, message.content);
});
}} />
<GitCloneButton
importChat={async (description, messages) => {
sendMessage?.(new Event('click') as any, description);
messages.forEach((message) => {
sendMessage?.(new Event('click') as any, message.content);
});
}}
/>
<ImportFolderButton
importChat={async (description, messages) => {
sendMessage?.(new Event('click') as any, description);
messages.forEach((message) => {
sendMessage?.(new Event('click') as any, message.content);
});
}}
/>
</div>
<div className="flex flex-col space-y-2 mt-4 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {

View File

@ -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)}
</button>
</WithTooltip>
);
}
}

View File

@ -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<boolean> => {
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
/>
<button
onClick={() => {
@ -147,4 +147,4 @@ ${fileArtifacts.join('\n\n')}
</div>
</WithTooltip>
);
};
};

View File

@ -14,11 +14,7 @@ const FrameworkLink = memo<FrameworkLinkProps>(({ template }) => (
className="items-center justify-center"
target="_self"
>
<img
src={template.icon}
alt={template.label}
className="w-8 h-8 opacity-25 hover:opacity-75 transition-all"
/>
<img src={template.icon} alt={template.label} className="w-8 h-8 opacity-25 hover:opacity-75 transition-all" />
</a>
));
@ -37,4 +33,4 @@ const StarterTemplates = memo(() => {
);
});
export default StarterTemplates;
export default StarterTemplates;

View File

@ -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 <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>;
}
}

View File

@ -6,7 +6,7 @@ interface WithTooltipProps {
children: React.ReactNode;
}
export default memo(function WithTooltip({ tooltip, children }: WithTooltipProps) {
export default memo(({ tooltip, children }: WithTooltipProps) => {
return (
<RadixTooltip.Provider>
<RadixTooltip.Root>
@ -23,4 +23,4 @@ export default memo(function WithTooltip({ tooltip, children }: WithTooltipProps
</RadixTooltip.Root>
</RadixTooltip.Provider>
);
});
});

View File

@ -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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
@ -97,4 +98,4 @@ export function GitHubPushModal({ isOpen, onClose }: GitHubPushModalProps) {
</div>
</div>
);
}
}

View File

@ -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 {

View File

@ -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<HTMLInputElement>(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) =>
<div className="i-ph:download-bold" />
Download
</PanelHeaderButton>
<PanelHeaderButton
className="mr-1 text-sm"
onClick={() => setShowGitHubModal(true)}
>
<PanelHeaderButton className="mr-1 text-sm" onClick={() => setShowGitHubModal(true)}>
<div className="i-ph:github-logo-bold" />
Push to GitHub
</PanelHeaderButton>
@ -244,17 +242,14 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
</div>
</div>
</div>
<GitHubPushModal
isOpen={showGitHubModal}
onClose={() => setShowGitHubModal(false)}
/>
<GitHubPushModal isOpen={showGitHubModal} onClose={() => setShowGitHubModal(false)} />
</motion.div>
)
);
});
interface ViewProps extends HTMLMotionProps<'div'> {
children: JSX.Element;
children: React.ReactElement;
}
const View = memo(({ children, ...props }: ViewProps) => {

View File

@ -20,8 +20,9 @@ export const IGNORE_PATTERNS = [
'**/yarn-error.log*',
'**/*lock.json',
'**/*lock.yaml',
// Binary files
// binary files
'**/*.jpg',
'**/*.jpeg',
'**/*.png',
];
];

View File

@ -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('/');
},
};
};

View File

@ -32,42 +32,48 @@ export function useChatHistory() {
const [ready, setReady] = useState<boolean>(false);
const [urlId, setUrlId] = useState<string | undefined>();
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,
};
}

View File

@ -12,12 +12,12 @@ class GitHubStore {
async pushToGitHub(token: string, username: string, repoName: string): Promise<void> {
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();
export const githubStore = new GitHubStore();

View File

@ -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<string, string>) {
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<string, string> = {};
@ -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);

View File

@ -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);
}

View File

@ -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) => {

View File

@ -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<typeof loader>();
return (
<div className="flex flex-col h-full w-full">
<Header />
<ClientOnly fallback={<BaseChat />}>{() => <GitUrlImport initialUrl={data.url} />}</ClientOnly>
</div>
);
}
}

View File

@ -20,10 +20,10 @@ export default function WebContainerPreview() {
const broadcastChannelRef = useRef<BroadcastChannel>();
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() {
/>
</div>
);
}
}

View File

@ -14,4 +14,4 @@ interface FileSystemFileHandle {
interface FileSystemWritableFileStream extends WritableStream {
write(data: string | BufferSource | Blob): Promise<void>;
close(): Promise<void>;
}
}

View File

@ -5,4 +5,4 @@ export interface Template {
githubRepo: string;
tags?: string[];
icon?: string;
}
}

View File

@ -13,35 +13,36 @@ interface DetectedCommand {
export async function detectProjectCommands(files: FileContent[]): Promise<DetectedCommand[]> {
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 =>
`<boltAction type="shell" title="${cmd.description}">
const commandsContent = commands
.map(
(cmd) =>
`<boltAction type="shell" title="${cmd.description}">
${cmd.command}
</boltAction>`
).join('\n');
</boltAction>`,
)
.join('\n');
return {
role: 'assistant',
@ -70,6 +74,6 @@ ${cmd.command}
${commandsContent}
</boltArtifact>`,
id: generateId(),
createdAt: new Date()
createdAt: new Date(),
};
}
}