mirror of
https://github.com/harivansh-afk/asap.it.git
synced 2026-04-17 15:04:51 +00:00
first commit
This commit is contained in:
commit
1cdbffff09
200 changed files with 30007 additions and 0 deletions
258
app/components/workbench/Workbench.client.tsx
Normal file
258
app/components/workbench/Workbench.client.tsx
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { useStore } from '@nanostores/react';
|
||||
import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
|
||||
import { computed } from 'nanostores';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
type OnChangeCallback as OnEditorChange,
|
||||
type OnScrollCallback as OnEditorScroll,
|
||||
} from '~/components/editor/codemirror/CodeMirrorEditor';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
||||
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
||||
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { renderLogger } from '~/utils/logger';
|
||||
import { EditorPanel } from './EditorPanel';
|
||||
import { Preview } from './Preview';
|
||||
import useViewport from '~/lib/hooks';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
interface WorkspaceProps {
|
||||
chatStarted?: boolean;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
const viewTransition = { ease: cubicEasingFn };
|
||||
|
||||
const sliderOptions: SliderOptions<WorkbenchViewType> = {
|
||||
left: {
|
||||
value: 'code',
|
||||
text: 'Code',
|
||||
},
|
||||
right: {
|
||||
value: 'preview',
|
||||
text: 'Preview',
|
||||
},
|
||||
};
|
||||
|
||||
const workbenchVariants = {
|
||||
closed: {
|
||||
width: 0,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: cubicEasingFn,
|
||||
},
|
||||
},
|
||||
open: {
|
||||
width: 'var(--workbench-width)',
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: cubicEasingFn,
|
||||
},
|
||||
},
|
||||
} satisfies Variants;
|
||||
|
||||
export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {
|
||||
renderLogger.trace('Workbench');
|
||||
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
|
||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||
const selectedFile = useStore(workbenchStore.selectedFile);
|
||||
const currentDocument = useStore(workbenchStore.currentDocument);
|
||||
const unsavedFiles = useStore(workbenchStore.unsavedFiles);
|
||||
const files = useStore(workbenchStore.files);
|
||||
const selectedView = useStore(workbenchStore.currentView);
|
||||
|
||||
const isSmallViewport = useViewport(1024);
|
||||
|
||||
const setSelectedView = (view: WorkbenchViewType) => {
|
||||
workbenchStore.currentView.set(view);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPreview) {
|
||||
setSelectedView('preview');
|
||||
}
|
||||
}, [hasPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
workbenchStore.setDocuments(files);
|
||||
}, [files]);
|
||||
|
||||
const onEditorChange = useCallback<OnEditorChange>((update) => {
|
||||
workbenchStore.setCurrentDocumentContent(update.content);
|
||||
}, []);
|
||||
|
||||
const onEditorScroll = useCallback<OnEditorScroll>((position) => {
|
||||
workbenchStore.setCurrentDocumentScrollPosition(position);
|
||||
}, []);
|
||||
|
||||
const onFileSelect = useCallback((filePath: string | undefined) => {
|
||||
workbenchStore.setSelectedFile(filePath);
|
||||
}, []);
|
||||
|
||||
const onFileSave = useCallback(() => {
|
||||
workbenchStore.saveCurrentDocument().catch(() => {
|
||||
toast.error('Failed to update file content');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onFileReset = useCallback(() => {
|
||||
workbenchStore.resetCurrentDocument();
|
||||
}, []);
|
||||
|
||||
const handleSyncFiles = useCallback(async () => {
|
||||
setIsSyncing(true);
|
||||
|
||||
try {
|
||||
const directoryHandle = await window.showDirectoryPicker();
|
||||
await workbenchStore.syncFiles(directoryHandle);
|
||||
toast.success('Files synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Error syncing files:', error);
|
||||
toast.error('Failed to sync files');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
chatStarted && (
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate={showWorkbench ? 'open' : 'closed'}
|
||||
variants={workbenchVariants}
|
||||
className="z-workbench"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
||||
{
|
||||
'w-full': isSmallViewport,
|
||||
'left-0': showWorkbench && isSmallViewport,
|
||||
'left-[var(--workbench-left)]': showWorkbench,
|
||||
'left-[100%]': !showWorkbench,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 px-2 lg:px-6">
|
||||
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||
<div className="ml-auto" />
|
||||
{selectedView === 'code' && (
|
||||
<div className="flex overflow-y-auto">
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.downloadZip();
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:code" />
|
||||
Download Code
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton className="mr-1 text-sm" onClick={handleSyncFiles} disabled={isSyncing}>
|
||||
{isSyncing ? <div className="i-ph:spinner" /> : <div className="i-ph:cloud-arrow-down" />}
|
||||
{isSyncing ? 'Syncing...' : 'Sync Files'}
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:terminal" />
|
||||
Toggle Terminal
|
||||
</PanelHeaderButton>
|
||||
<PanelHeaderButton
|
||||
className="mr-1 text-sm"
|
||||
onClick={() => {
|
||||
const repoName = prompt(
|
||||
'Please enter a name for your new GitHub repository:',
|
||||
'bolt-generated-project',
|
||||
);
|
||||
|
||||
if (!repoName) {
|
||||
alert('Repository name is required. Push to GitHub cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
const githubUsername = Cookies.get('githubUsername');
|
||||
const githubToken = Cookies.get('githubToken');
|
||||
|
||||
if (!githubUsername || !githubToken) {
|
||||
const usernameInput = prompt('Please enter your GitHub username:');
|
||||
const tokenInput = prompt('Please enter your GitHub personal access token:');
|
||||
|
||||
if (!usernameInput || !tokenInput) {
|
||||
alert('GitHub username and token are required. Push to GitHub cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput);
|
||||
} else {
|
||||
workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="i-ph:github-logo" />
|
||||
Push to GitHub
|
||||
</PanelHeaderButton>
|
||||
</div>
|
||||
)}
|
||||
<IconButton
|
||||
icon="i-ph:x-circle"
|
||||
className="-mr-1"
|
||||
size="xl"
|
||||
onClick={() => {
|
||||
workbenchStore.showWorkbench.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
<View
|
||||
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||
>
|
||||
<EditorPanel
|
||||
editorDocument={currentDocument}
|
||||
isStreaming={isStreaming}
|
||||
selectedFile={selectedFile}
|
||||
files={files}
|
||||
unsavedFiles={unsavedFiles}
|
||||
onFileSelect={onFileSelect}
|
||||
onEditorScroll={onEditorScroll}
|
||||
onEditorChange={onEditorChange}
|
||||
onFileSave={onFileSave}
|
||||
onFileReset={onFileReset}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||
>
|
||||
<Preview />
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
});
|
||||
interface ViewProps extends HTMLMotionProps<'div'> {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const View = memo(({ children, ...props }: ViewProps) => {
|
||||
return (
|
||||
<motion.div className="absolute inset-0" transition={viewTransition} {...props}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue