mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
chore: lint fix
completed lint fixes
This commit is contained in:
commit
d84ae2ab19
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -20,8 +20,9 @@ export const IGNORE_PATTERNS = [
|
||||
'**/yarn-error.log*',
|
||||
'**/*lock.json',
|
||||
'**/*lock.yaml',
|
||||
// Binary files
|
||||
|
||||
// binary files
|
||||
'**/*.jpg',
|
||||
'**/*.jpeg',
|
||||
'**/*.png',
|
||||
];
|
||||
];
|
||||
|
||||
@ -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('/');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
2
app/types/global.d.ts
vendored
2
app/types/global.d.ts
vendored
@ -14,4 +14,4 @@ interface FileSystemFileHandle {
|
||||
interface FileSystemWritableFileStream extends WritableStream {
|
||||
write(data: string | BufferSource | Blob): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,4 +5,4 @@ export interface Template {
|
||||
githubRepo: string;
|
||||
tags?: string[];
|
||||
icon?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user