initial
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
export interface CodeApplicationState {
|
||||
stage: 'analyzing' | 'installing' | 'applying' | 'complete' | null;
|
||||
packages?: string[];
|
||||
installedPackages?: string[];
|
||||
filesGenerated?: string[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface CodeApplicationProgressProps {
|
||||
state: CodeApplicationState;
|
||||
}
|
||||
|
||||
export default function CodeApplicationProgress({ state }: CodeApplicationProgressProps) {
|
||||
if (!state.stage || state.stage === 'complete') return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="inline-block bg-gray-100 rounded-[10px] p-3 mt-2"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Rotating loading indicator */}
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<svg className="w-full h-full" viewBox="0 0 24 24" fill="none">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="31.416"
|
||||
strokeDashoffset="10"
|
||||
className="text-gray-700"
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
|
||||
{/* Simple loading text */}
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
Applying to sandbox...
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface HMRErrorDetectorProps {
|
||||
iframeRef: React.RefObject<HTMLIFrameElement>;
|
||||
onErrorDetected: (errors: Array<{ type: string; message: string; package?: string }>) => void;
|
||||
}
|
||||
|
||||
export default function HMRErrorDetector({ iframeRef, onErrorDetected }: HMRErrorDetectorProps) {
|
||||
const checkIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkForHMRErrors = () => {
|
||||
if (!iframeRef.current) return;
|
||||
|
||||
try {
|
||||
const iframeDoc = iframeRef.current.contentDocument;
|
||||
if (!iframeDoc) return;
|
||||
|
||||
// Check for Vite error overlay
|
||||
const errorOverlay = iframeDoc.querySelector('vite-error-overlay');
|
||||
if (errorOverlay) {
|
||||
// Try to extract error message
|
||||
const messageElement = errorOverlay.shadowRoot?.querySelector('.message-body');
|
||||
if (messageElement) {
|
||||
const errorText = messageElement.textContent || '';
|
||||
|
||||
// Parse import errors
|
||||
const importMatch = errorText.match(/Failed to resolve import "([^"]+)"/);
|
||||
if (importMatch) {
|
||||
const packageName = importMatch[1];
|
||||
if (!packageName.startsWith('.')) {
|
||||
// Extract base package name
|
||||
let finalPackage = packageName;
|
||||
if (packageName.startsWith('@')) {
|
||||
const parts = packageName.split('/');
|
||||
finalPackage = parts.length >= 2 ? parts.slice(0, 2).join('/') : packageName;
|
||||
} else {
|
||||
finalPackage = packageName.split('/')[0];
|
||||
}
|
||||
|
||||
onErrorDetected([{
|
||||
type: 'npm-missing',
|
||||
message: `Failed to resolve import "${packageName}"`,
|
||||
package: finalPackage
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Cross-origin errors are expected, ignore them
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately and then every 2 seconds
|
||||
checkForHMRErrors();
|
||||
checkIntervalRef.current = setInterval(checkForHMRErrors, 2000);
|
||||
|
||||
return () => {
|
||||
if (checkIntervalRef.current) {
|
||||
clearInterval(checkIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [iframeRef, onErrorDetected]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react';
|
||||
|
||||
interface SandboxPreviewProps {
|
||||
sandboxId: string;
|
||||
port: number;
|
||||
type: 'vite' | 'nextjs' | 'console';
|
||||
output?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function SandboxPreview({
|
||||
sandboxId,
|
||||
port,
|
||||
type,
|
||||
output,
|
||||
isLoading = false
|
||||
}: SandboxPreviewProps) {
|
||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
||||
const [showConsole, setShowConsole] = useState(false);
|
||||
const [iframeKey, setIframeKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (sandboxId && type !== 'console') {
|
||||
// In production, this would be the actual E2B sandbox URL
|
||||
// Format: https://{sandboxId}-{port}.e2b.dev
|
||||
setPreviewUrl(`https://${sandboxId}-${port}.e2b.dev`);
|
||||
}
|
||||
}, [sandboxId, port, type]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setIframeKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
if (type === 'console') {
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||
<div className="font-mono text-sm whitespace-pre-wrap text-gray-300">
|
||||
{output || 'No output yet...'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Preview Controls */}
|
||||
<div className="flex items-center justify-between bg-gray-800 rounded-lg p-3 border border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-400">
|
||||
{type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview
|
||||
</span>
|
||||
<code className="text-xs bg-gray-900 px-2 py-1 rounded text-blue-400">
|
||||
{previewUrl}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowConsole(!showConsole)}
|
||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
||||
title="Toggle console"
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
||||
title="Refresh preview"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<a
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
||||
title="Open in new tab"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Preview */}
|
||||
<div className="relative bg-gray-900 rounded-lg overflow-hidden border border-gray-700">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 bg-gray-900/80 flex items-center justify-center z-10">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-400">
|
||||
{type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
src={previewUrl}
|
||||
className="w-full h-[600px] bg-white"
|
||||
title={`${type} preview`}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Console Output (Toggle) */}
|
||||
{showConsole && output && (
|
||||
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-semibold text-gray-400">Console Output</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs whitespace-pre-wrap text-gray-300 max-h-48 overflow-y-auto">
|
||||
{output}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-[10px] text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-zinc-900 text-white hover:bg-zinc-800 [box-shadow:inset_0px_-2px_0px_0px_#18181b,_0px_1px_6px_0px_rgba(24,_24,_27,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#18181b,_0px_1px_3px_0px_rgba(24,_24,_27,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#18181b,_0px_1px_2px_0px_rgba(24,_24,_27,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
|
||||
secondary: "bg-zinc-100 text-zinc-900 hover:bg-zinc-200 [box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(161,_161,_170,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#d4d4d8,_0px_1px_3px_0px_rgba(161,_161,_170,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#d4d4d8,_0px_1px_2px_0px_rgba(161,_161,_170,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
|
||||
outline: "border border-zinc-300 bg-transparent hover:bg-zinc-50 text-zinc-900 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#e4e4e7,_0px_1px_3px_0px_rgba(228,_228,_231,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#e4e4e7,_0px_1px_2px_0px_rgba(228,_228,_231,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
|
||||
destructive: "bg-red-500 text-white hover:bg-red-600 [box-shadow:inset_0px_-2px_0px_0px_#dc2626,_0px_1px_6px_0px_rgba(239,_68,_68,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#dc2626,_0px_1px_3px_0px_rgba(239,_68,_68,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#dc2626,_0px_1px_2px_0px_rgba(239,_68,_68,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
|
||||
code: "bg-[#36322F] text-white hover:bg-[#4a4542] [box-shadow:inset_0px_-2px_0px_0px_#171310,_0px_1px_6px_0px_rgba(58,_33,_8,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#171310,_0px_1px_3px_0px_rgba(58,_33,_8,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#171310,_0px_1px_2px_0px_rgba(58,_33,_8,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
|
||||
orange: "bg-orange-500 text-white hover:bg-orange-600 [box-shadow:inset_0px_-2px_0px_0px_#c2410c,_0px_1px_6px_0px_rgba(234,_88,_12,_58%)] hover:translate-y-[1px] hover:scale-[0.98] hover:[box-shadow:inset_0px_-1px_0px_0px_#c2410c,_0px_1px_3px_0px_rgba(234,_88,_12,_40%)] active:translate-y-[2px] active:scale-[0.97] active:[box-shadow:inset_0px_1px_1px_0px_#c2410c,_0px_1px_2px_0px_rgba(234,_88,_12,_30%)] disabled:shadow-none disabled:hover:translate-y-0 disabled:hover:scale-100",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-8 px-3 py-1 text-sm",
|
||||
lg: "h-12 px-6 py-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? "button" : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as React from "react"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface CheckboxProps {
|
||||
label?: string
|
||||
defaultChecked?: boolean
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
onChange?: (checked: boolean) => void
|
||||
}
|
||||
|
||||
const Checkbox = React.forwardRef<HTMLDivElement, CheckboxProps>(
|
||||
({ label, defaultChecked = false, disabled = false, className, onChange }, ref) => {
|
||||
const [checked, setChecked] = React.useState(defaultChecked)
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!disabled) {
|
||||
const newChecked = !checked
|
||||
setChecked(newChecked)
|
||||
onChange?.(newChecked)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center gap-2", className)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"h-4 w-4 rounded border border-zinc-300 flex items-center justify-center transition-all duration-200",
|
||||
"[box-shadow:inset_0px_-1px_0px_0px_#e4e4e7,_0px_1px_3px_0px_rgba(228,_228,_231,_20%)]",
|
||||
!disabled && "hover:[box-shadow:inset_0px_-1px_0px_0px_#d4d4d8,_0px_1px_3px_0px_rgba(212,_212,_216,_30%)]",
|
||||
checked && "bg-orange-500 border-orange-500 [box-shadow:inset_0px_-1px_0px_0px_#c2410c,_0px_1px_3px_0px_rgba(234,_88,_12,_30%)]",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{checked && <Check className="h-3 w-3 text-white" />}
|
||||
</button>
|
||||
{label && (
|
||||
<label
|
||||
onClick={handleToggle}
|
||||
className={cn(
|
||||
"text-sm select-none",
|
||||
!disabled && "cursor-pointer",
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Checkbox.displayName = "Checkbox"
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-[10px] border border-zinc-300 bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_30%)] hover:[box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(212,_212,_216,_40%)] focus-visible:[box-shadow:inset_0px_-2px_0px_0px_#f97316,_0px_1px_6px_0px_rgba(249,_115,_22,_30%)] transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
HTMLLabelElement,
|
||||
React.ComponentPropsWithoutRef<"label"> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = "Label"
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<select
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-[10px] border border-zinc-300 bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_30%)] hover:[box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(212,_212,_216,_40%)] focus-visible:[box-shadow:inset_0px_-2px_0px_0px_#f97316,_0px_1px_6px_0px_rgba(249,_115,_22,_30%)] transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
)
|
||||
Select.displayName = "Select"
|
||||
|
||||
export { Select }
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-[10px] border border-zinc-300 bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [box-shadow:inset_0px_-2px_0px_0px_#e4e4e7,_0px_1px_6px_0px_rgba(228,_228,_231,_30%)] hover:[box-shadow:inset_0px_-2px_0px_0px_#d4d4d8,_0px_1px_6px_0px_rgba(212,_212,_216,_40%)] focus-visible:[box-shadow:inset_0px_-2px_0px_0px_#f97316,_0px_1px_6px_0px_rgba(249,_115,_22,_30%)] transition-all duration-200",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
Reference in New Issue
Block a user