Merge branch 'main' of https://github.com/Code-Victor/sandbox into feat/light-theme

This commit is contained in:
Hamzat Victor 2024-10-23 11:00:24 +01:00
commit 9546d4ad7b
63 changed files with 1325 additions and 1112 deletions

View File

@ -1,4 +1,3 @@
# frontend/**
backend/ai/**
backend/database/**
backend/storage/**

View File

@ -5,3 +5,6 @@ compatibility_flags = ["nodejs_compat"]
[ai]
binding = "AI"
[vars]
ANTHROPIC_API_KEY = ""

View File

@ -207,7 +207,7 @@ io.on("connection", async (socket) => {
}
} catch (e: any) {
console.error(`Error creating container ${data.sandboxId}:`, e)
io.emit("error", `Error: container creation. ${e.message ?? e}`)
socket.emit("error", `Error: container creation. ${e.message ?? e}`)
}
}
)
@ -244,7 +244,7 @@ io.on("connection", async (socket) => {
await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT)
} catch (e: any) {
console.error("Error setting timeout:", e)
io.emit("error", `Error: set timeout. ${e.message ?? e}`)
socket.emit("error", `Error: set timeout. ${e.message ?? e}`)
}
})
@ -255,7 +255,7 @@ io.on("connection", async (socket) => {
callback(fileContent)
} catch (e: any) {
console.error("Error getting file:", e)
io.emit("error", `Error: get file. ${e.message ?? e}`)
socket.emit("error", `Error: get file. ${e.message ?? e}`)
}
})
@ -266,7 +266,7 @@ io.on("connection", async (socket) => {
callback(files)
} catch (e: any) {
console.error("Error getting folder:", e)
io.emit("error", `Error: get folder. ${e.message ?? e}`)
socket.emit("error", `Error: get folder. ${e.message ?? e}`)
}
})
@ -277,7 +277,7 @@ io.on("connection", async (socket) => {
await fileManager.saveFile(fileId, body)
} catch (e: any) {
console.error("Error saving file:", e)
io.emit("error", `Error: file saving. ${e.message ?? e}`)
socket.emit("error", `Error: file saving. ${e.message ?? e}`)
}
})
@ -290,7 +290,7 @@ io.on("connection", async (socket) => {
callback(newFiles)
} catch (e: any) {
console.error("Error moving file:", e)
io.emit("error", `Error: file moving. ${e.message ?? e}`)
socket.emit("error", `Error: file moving. ${e.message ?? e}`)
}
}
)
@ -361,7 +361,7 @@ io.on("connection", async (socket) => {
callback({ success })
} catch (e: any) {
console.error("Error creating file:", e)
io.emit("error", `Error: file creation. ${e.message ?? e}`)
socket.emit("error", `Error: file creation. ${e.message ?? e}`)
}
})
@ -373,7 +373,7 @@ io.on("connection", async (socket) => {
callback()
} catch (e: any) {
console.error("Error creating folder:", e)
io.emit("error", `Error: folder creation. ${e.message ?? e}`)
socket.emit("error", `Error: folder creation. ${e.message ?? e}`)
}
})
@ -384,7 +384,7 @@ io.on("connection", async (socket) => {
await fileManager.renameFile(fileId, newName)
} catch (e: any) {
console.error("Error renaming file:", e)
io.emit("error", `Error: file renaming. ${e.message ?? e}`)
socket.emit("error", `Error: file renaming. ${e.message ?? e}`)
}
})
@ -396,7 +396,7 @@ io.on("connection", async (socket) => {
callback(newFiles)
} catch (e: any) {
console.error("Error deleting file:", e)
io.emit("error", `Error: file deletion. ${e.message ?? e}`)
socket.emit("error", `Error: file deletion. ${e.message ?? e}`)
}
})
@ -407,7 +407,7 @@ io.on("connection", async (socket) => {
callback(newFiles)
} catch (e: any) {
console.error("Error deleting folder:", e)
io.emit("error", `Error: folder deletion. ${e.message ?? e}`)
socket.emit("error", `Error: folder deletion. ${e.message ?? e}`)
}
})
@ -416,10 +416,10 @@ io.on("connection", async (socket) => {
try {
await lockManager.acquireLock(data.sandboxId, async () => {
await terminalManager.createTerminal(id, (responseString: string) => {
io.emit("terminalResponse", { id, data: responseString })
socket.emit("terminalResponse", { id, data: responseString })
const port = extractPortNumber(responseString)
if (port) {
io.emit(
socket.emit(
"previewURL",
"https://" + containers[data.sandboxId].getHost(port)
)
@ -429,7 +429,7 @@ io.on("connection", async (socket) => {
callback()
} catch (e: any) {
console.error(`Error creating terminal ${id}:`, e)
io.emit("error", `Error: terminal creation. ${e.message ?? e}`)
socket.emit("error", `Error: terminal creation. ${e.message ?? e}`)
}
})
@ -441,7 +441,7 @@ io.on("connection", async (socket) => {
terminalManager.resizeTerminal(dimensions)
} catch (e: any) {
console.error("Error resizing terminal:", e)
io.emit("error", `Error: terminal resizing. ${e.message ?? e}`)
socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`)
}
}
)
@ -452,7 +452,7 @@ io.on("connection", async (socket) => {
await terminalManager.sendTerminalData(id, data)
} catch (e: any) {
console.error("Error writing to terminal:", e)
io.emit("error", `Error: writing to terminal. ${e.message ?? e}`)
socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`)
}
})
@ -463,7 +463,7 @@ io.on("connection", async (socket) => {
callback()
} catch (e: any) {
console.error("Error closing terminal:", e)
io.emit("error", `Error: closing terminal. ${e.message ?? e}`)
socket.emit("error", `Error: closing terminal. ${e.message ?? e}`)
}
})
@ -488,7 +488,7 @@ io.on("connection", async (socket) => {
callback(result)
} catch (e: any) {
console.error("Error generating code:", e)
io.emit("error", `Error: code generation. ${e.message ?? e}`)
socket.emit("error", `Error: code generation. ${e.message ?? e}`)
}
}
)
@ -511,12 +511,12 @@ io.on("connection", async (socket) => {
}
} catch (e: any) {
console.log("Error disconnecting:", e)
io.emit("error", `Error: disconnecting. ${e.message ?? e}`)
socket.emit("error", `Error: disconnecting. ${e.message ?? e}`)
}
})
} catch (e: any) {
console.error("Error connecting:", e)
io.emit("error", `Error: connection. ${e.message ?? e}`)
socket.emit("error", `Error: connection. ${e.message ?? e}`)
}
})

View File

@ -1,5 +1,6 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false
"singleQuote": false,
"insertFinalNewline": true
}

View File

@ -1,12 +1,11 @@
import Navbar from "@/components/editor/navbar"
import { Room } from "@/components/editor/live/room"
import Loading from "@/components/editor/loading"
import Navbar from "@/components/editor/navbar"
import { TerminalProvider } from "@/context/TerminalContext"
import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { notFound, redirect } from "next/navigation"
import Loading from "@/components/editor/loading"
import dynamic from "next/dynamic"
import fs from "fs"
import { TerminalProvider } from "@/context/TerminalContext"
import { notFound, redirect } from "next/navigation"
export const revalidate = 0
@ -89,19 +88,20 @@ export default async function CodePage({ params }: { params: { id: string } }) {
return (
<>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}>
<TerminalProvider>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow">
<CodeEditor
userData={userData}
sandboxData={sandboxData}
/>
</div>
</TerminalProvider>
</Room>
</div>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}>
<TerminalProvider>
<Navbar
userData={userData}
sandboxData={sandboxData}
shared={shared}
/>
<div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxData={sandboxData} />
</div>
</TerminalProvider>
</Room>
</div>
</>
)
}

View File

@ -1,8 +1,8 @@
import { UserButton, currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
import Dashboard from "@/components/dashboard"
import Navbar from "@/components/dashboard/navbar"
import { Sandbox, User } from "@/lib/types"
import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
export default async function DashboardPage() {
const user = await currentUser()

View File

@ -15,7 +15,7 @@ export const metadata: Metadata = {
}
export default function RootLayout({
children
children,
}: Readonly<{
children: React.ReactNode
}>) {
@ -29,9 +29,7 @@ export default function RootLayout({
disableTransitionOnChange
>
<SocketProvider>
<PreviewProvider>
{children}
</PreviewProvider>
<PreviewProvider>{children}</PreviewProvider>
</SocketProvider>
<Analytics />
<Toaster position="bottom-left" richColors />
@ -40,4 +38,4 @@ export default function RootLayout({
</html>
</ClerkProvider>
)
}
}

View File

@ -1,13 +1,13 @@
import { currentUser } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import Landing from "@/components/landing";
import Landing from "@/components/landing"
import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
export default async function Home() {
const user = await currentUser();
const user = await currentUser()
if (user) {
redirect("/dashboard");
redirect("/dashboard")
}
return <Landing />;
return <Landing />
}

View File

@ -3,16 +3,9 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import Image from "next/image"
import { useState } from "react"
import { Button } from "../ui/button"
import { ChevronRight } from "lucide-react"
export default function AboutModal({
open,

View File

@ -1,24 +1,16 @@
"use client"
import CustomButton from "@/components/ui/customButton"
import { Button } from "@/components/ui/button"
import {
Code2,
FolderDot,
HelpCircle,
Plus,
Settings,
Users,
} from "lucide-react"
import { useEffect, useState } from "react"
import CustomButton from "@/components/ui/customButton"
import { Sandbox } from "@/lib/types"
import { Code2, FolderDot, HelpCircle, Plus, Users } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import AboutModal from "./about"
import NewProjectModal from "./newProject"
import DashboardProjects from "./projects"
import DashboardSharedWithMe from "./shared"
import NewProjectModal from "./newProject"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import AboutModal from "./about"
import { toast } from "sonner"
type TScreen = "projects" | "shared" | "settings" | "search"
@ -49,8 +41,9 @@ export default function Dashboard({
const q = searchParams.get("q")
const router = useRouter()
useEffect(() => { // update the dashboard to show a new project
router.refresh()
useEffect(() => {
// update the dashboard to show a new project
router.refresh()
}, [])
return (

View File

@ -1,9 +1,9 @@
import Logo from "@/assets/logo.svg"
import { User } from "@/lib/types"
import Image from "next/image"
import Link from "next/link"
import Logo from "@/assets/logo.svg"
import DashboardNavbarSearch from "./search"
import UserButton from "../../ui/userButton"
import { User } from "@/lib/types"
import DashboardNavbarSearch from "./search"
export default function DashboardNavbar({ userData }: { userData: User }) {
return (

View File

@ -1,13 +1,12 @@
"use client";
"use client"
import { Input } from "../../ui/input";
import { Search } from "lucide-react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Search } from "lucide-react"
import { useRouter } from "next/navigation"
import { Input } from "../../ui/input"
export default function DashboardNavbarSearch() {
// const [search, setSearch] = useState("");
const router = useRouter();
const router = useRouter()
// useEffect(() => {
// const delayDebounceFn = setTimeout(() => {
@ -29,14 +28,14 @@ export default function DashboardNavbarSearch() {
// onChange={(e) => setSearch(e.target.value)}
onChange={(e) => {
if (e.target.value === "") {
router.push(`/dashboard`);
return;
router.push(`/dashboard`)
return
}
router.push(`/dashboard?q=${e.target.value}`);
router.push(`/dashboard?q=${e.target.value}`)
}}
placeholder="Search projects..."
className="pl-8"
/>
</div>
);
)
}

View File

@ -1,30 +1,30 @@
"use client";
"use client"
import { Sandbox } from "@/lib/types";
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react";
import { Sandbox } from "@/lib/types"
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
} from "@/components/ui/dropdown-menu"
export default function ProjectCardDropdown({
sandbox,
onVisibilityChange,
onDelete,
}: {
sandbox: Sandbox;
onVisibilityChange: (sandbox: Sandbox) => void;
onDelete: (sandbox: Sandbox) => void;
sandbox: Sandbox
onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void
}) {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
e.preventDefault()
e.stopPropagation()
}}
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
>
@ -33,8 +33,8 @@ export default function ProjectCardDropdown({
<DropdownMenuContent className="w-40">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onVisibilityChange(sandbox);
e.stopPropagation()
onVisibilityChange(sandbox)
}}
className="cursor-pointer"
>
@ -52,8 +52,8 @@ export default function ProjectCardDropdown({
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete(sandbox);
e.stopPropagation()
onDelete(sandbox)
}}
className="!text-destructive cursor-pointer"
>
@ -62,5 +62,5 @@ export default function ProjectCardDropdown({
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
)
}

View File

@ -1,14 +1,14 @@
"use client"
import { Card } from "@/components/ui/card"
import { projectTemplates } from "@/lib/data"
import { Sandbox } from "@/lib/types"
import { AnimatePresence, motion } from "framer-motion"
import { Clock, Globe, Lock } from "lucide-react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import ProjectCardDropdown from "./dropdown"
import { Clock, Globe, Lock } from "lucide-react"
import { Sandbox } from "@/lib/types"
import { Card } from "@/components/ui/card"
import { useRouter } from "next/navigation"
import { projectTemplates } from "@/lib/data"
export default function ProjectCard({
children,

View File

@ -1,8 +1,8 @@
"use client";
import { cn } from "@/lib/utils";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import React, { useMemo, useRef } from "react";
import * as THREE from "three";
"use client"
import { cn } from "@/lib/utils"
import { Canvas, useFrame, useThree } from "@react-three/fiber"
import React, { useMemo, useRef } from "react"
import * as THREE from "three"
export const CanvasRevealEffect = ({
animationSpeed = 0.4,
@ -12,12 +12,12 @@ export const CanvasRevealEffect = ({
dotSize,
showGradient = true,
}: {
animationSpeed?: number;
opacities?: number[];
colors?: number[][];
containerClassName?: string;
dotSize?: number;
showGradient?: boolean;
animationSpeed?: number
opacities?: number[]
colors?: number[][]
containerClassName?: string
dotSize?: number
showGradient?: boolean
}) => {
return (
<div className={cn("h-full relative bg-white w-full", containerClassName)}>
@ -41,16 +41,16 @@ export const CanvasRevealEffect = ({
<div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" />
)}
</div>
);
};
)
}
interface DotMatrixProps {
colors?: number[][];
opacities?: number[];
totalSize?: number;
dotSize?: number;
shader?: string;
center?: ("x" | "y")[];
colors?: number[][]
opacities?: number[]
totalSize?: number
dotSize?: number
shader?: string
center?: ("x" | "y")[]
}
const DotMatrix: React.FC<DotMatrixProps> = ({
@ -69,7 +69,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[0],
colors[0],
colors[0],
];
]
if (colors.length === 2) {
colorsArray = [
colors[0],
@ -78,7 +78,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[1],
colors[1],
colors[1],
];
]
} else if (colors.length === 3) {
colorsArray = [
colors[0],
@ -87,7 +87,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[1],
colors[2],
colors[2],
];
]
}
return {
@ -111,8 +111,8 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
value: dotSize,
type: "uniform1f",
},
};
}, [colors, opacities, totalSize, dotSize]);
}
}, [colors, opacities, totalSize, dotSize])
return (
<Shader
@ -168,87 +168,87 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
uniforms={uniforms}
maxFps={60}
/>
);
};
)
}
type Uniforms = {
[key: string]: {
value: number[] | number[][] | number;
type: string;
};
};
value: number[] | number[][] | number
type: string
}
}
const ShaderMaterial = ({
source,
uniforms,
maxFps = 60,
}: {
source: string;
hovered?: boolean;
maxFps?: number;
uniforms: Uniforms;
source: string
hovered?: boolean
maxFps?: number
uniforms: Uniforms
}) => {
const { size } = useThree();
const ref = useRef<THREE.Mesh>();
let lastFrameTime = 0;
const { size } = useThree()
const ref = useRef<THREE.Mesh>()
let lastFrameTime = 0
useFrame(({ clock }) => {
if (!ref.current) return;
const timestamp = clock.getElapsedTime();
if (!ref.current) return
const timestamp = clock.getElapsedTime()
if (timestamp - lastFrameTime < 1 / maxFps) {
return;
return
}
lastFrameTime = timestamp;
lastFrameTime = timestamp
const material: any = ref.current.material;
const timeLocation = material.uniforms.u_time;
timeLocation.value = timestamp;
});
const material: any = ref.current.material
const timeLocation = material.uniforms.u_time
timeLocation.value = timestamp
})
const getUniforms = () => {
const preparedUniforms: any = {};
const preparedUniforms: any = {}
for (const uniformName in uniforms) {
const uniform: any = uniforms[uniformName];
const uniform: any = uniforms[uniformName]
switch (uniform.type) {
case "uniform1f":
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
break;
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" }
break
case "uniform3f":
preparedUniforms[uniformName] = {
value: new THREE.Vector3().fromArray(uniform.value),
type: "3f",
};
break;
}
break
case "uniform1fv":
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
break;
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }
break
case "uniform3fv":
preparedUniforms[uniformName] = {
value: uniform.value.map((v: number[]) =>
new THREE.Vector3().fromArray(v)
),
type: "3fv",
};
break;
}
break
case "uniform2f":
preparedUniforms[uniformName] = {
value: new THREE.Vector2().fromArray(uniform.value),
type: "2f",
};
break;
}
break
default:
console.error(`Invalid uniform type for '${uniformName}'.`);
break;
console.error(`Invalid uniform type for '${uniformName}'.`)
break
}
}
preparedUniforms["u_time"] = { value: 0, type: "1f" };
preparedUniforms["u_time"] = { value: 0, type: "1f" }
preparedUniforms["u_resolution"] = {
value: new THREE.Vector2(size.width * 2, size.height * 2),
}; // Initialize u_resolution
return preparedUniforms;
};
} // Initialize u_resolution
return preparedUniforms
}
// Shader material
const material = useMemo(() => {
@ -272,33 +272,33 @@ const ShaderMaterial = ({
blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneFactor,
});
})
return materialObject;
}, [size.width, size.height, source]);
return materialObject
}, [size.width, size.height, source])
return (
<mesh ref={ref as any}>
<planeGeometry args={[2, 2]} />
<primitive object={material} attach="material" />
</mesh>
);
};
)
}
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
return (
<Canvas className="absolute inset-0 h-full w-full">
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
</Canvas>
);
};
)
}
interface ShaderProps {
source: string;
source: string
uniforms: {
[key: string]: {
value: number[] | number[][] | number;
type: string;
};
};
maxFps?: number;
value: number[] | number[][] | number
type: string
}
}
maxFps?: number
}

View File

@ -1,16 +1,12 @@
"use client";
"use client"
import { Sandbox } from "@/lib/types";
import ProjectCard from "./projectCard";
import Image from "next/image";
import ProjectCardDropdown from "./projectCard/dropdown";
import { Clock, Globe, Lock } from "lucide-react";
import Link from "next/link";
import { Card } from "../ui/card";
import { deleteSandbox, updateSandbox } from "@/lib/actions";
import { toast } from "sonner";
import { useEffect, useState } from "react";
import { CanvasRevealEffect } from "./projectCard/revealEffect";
import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { Sandbox } from "@/lib/types"
import Link from "next/link"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import ProjectCard from "./projectCard"
import { CanvasRevealEffect } from "./projectCard/revealEffect"
const colors: { [key: string]: number[][] } = {
react: [
@ -21,38 +17,37 @@ const colors: { [key: string]: number[][] } = {
[86, 184, 72],
[59, 112, 52],
],
};
}
export default function DashboardProjects({
sandboxes,
q,
}: {
sandboxes: Sandbox[];
q: string | null;
sandboxes: Sandbox[]
q: string | null
}) {
const [deletingId, setDeletingId] = useState<string>("");
const [deletingId, setDeletingId] = useState<string>("")
const onDelete = async (sandbox: Sandbox) => {
setDeletingId(sandbox.id);
toast(`Project ${sandbox.name} deleted.`);
await deleteSandbox(sandbox.id);
};
setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id)
}
useEffect(() => {
if (deletingId) {
setDeletingId("");
setDeletingId("")
}
}, [sandboxes]);
}, [sandboxes])
const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility =
sandbox.visibility === "public" ? "private" : "public";
toast(`Project ${sandbox.name} is now ${newVisibility}.`);
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
});
};
})
}
return (
<div className="grow p-4 flex flex-col">
@ -65,7 +60,7 @@ export default function DashboardProjects({
{sandboxes.map((sandbox) => {
if (q && q.length > 0) {
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
return null;
return null
}
}
return (
@ -93,7 +88,7 @@ export default function DashboardProjects({
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard>
</Link>
);
)
})}
</div>
) : (
@ -103,5 +98,5 @@ export default function DashboardProjects({
)}
</div>
</div>
);
)
}

View File

@ -1,29 +1,27 @@
import { Sandbox } from "@/lib/types";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Image from "next/image";
import Button from "../ui/customButton";
import { ChevronRight } from "lucide-react";
import Avatar from "../ui/avatar";
import Link from "next/link";
} from "@/components/ui/table"
import { ChevronRight } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import Avatar from "../ui/avatar"
import Button from "../ui/customButton"
export default function DashboardSharedWithMe({
shared,
}: {
shared: {
id: string;
name: string;
type: "react" | "node";
author: string;
sharedOn: Date;
}[];
id: string
name: string
type: "react" | "node"
author: string
sharedOn: Date
}[]
}) {
return (
<div className="grow p-4 flex flex-col">
@ -86,5 +84,5 @@ export default function DashboardSharedWithMe({
</div>
)}
</div>
);
)
}

View File

@ -1,36 +1,51 @@
import React from 'react';
import { Button } from '../../ui/button';
import { Send, StopCircle } from 'lucide-react';
import { Send, StopCircle } from "lucide-react"
import { Button } from "../../ui/button"
interface ChatInputProps {
input: string;
setInput: (input: string) => void;
isGenerating: boolean;
handleSend: () => void;
handleStopGeneration: () => void;
input: string
setInput: (input: string) => void
isGenerating: boolean
handleSend: () => void
handleStopGeneration: () => void
}
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) {
export default function ChatInput({
input,
setInput,
isGenerating,
handleSend,
handleStopGeneration,
}: ChatInputProps) {
return (
<div className="flex space-x-2 min-w-0">
<input
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()}
onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
placeholder="Type your message..."
disabled={isGenerating}
/>
{isGenerating ? (
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10">
<Button
onClick={handleStopGeneration}
variant="destructive"
size="icon"
className="h-10 w-10"
>
<StopCircle className="w-4 h-4" />
</Button>
) : (
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10">
<Button
onClick={handleSend}
disabled={isGenerating}
size="icon"
className="h-10 w-10"
>
<Send className="w-4 h-4" />
</Button>
)}
</div>
);
)
}

View File

@ -1,25 +1,31 @@
import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
import { copyToClipboard, stringifyContent } from './lib/chatUtils';
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
import React, { useState } from "react"
import ReactMarkdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
import remarkGfm from "remark-gfm"
import { Button } from "../../ui/button"
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
interface MessageProps {
message: {
role: 'user' | 'assistant';
content: string;
context?: string;
};
setContext: (context: string | null) => void;
setIsContextExpanded: (isExpanded: boolean) => void;
role: "user" | "assistant"
content: string
context?: string
}
setContext: (context: string | null) => void
setIsContextExpanded: (isExpanded: boolean) => void
}
export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) {
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
const [copiedText, setCopiedText] = useState<string | null>(null);
export default function ChatMessage({
message,
setContext,
setIsContextExpanded,
}: MessageProps) {
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
number | null
>(null)
const [copiedText, setCopiedText] = useState<string | null>(null)
const renderCopyButton = (text: any) => (
<Button
@ -34,17 +40,17 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
<Copy className="w-4 h-4" />
)}
</Button>
);
)
const askAboutCode = (code: any) => {
const contextString = stringifyContent(code);
setContext(`Regarding this code:\n${contextString}`);
setIsContextExpanded(false);
};
const contextString = stringifyContent(code)
setContext(`Regarding this code:\n${contextString}`)
setIsContextExpanded(false)
}
const renderMarkdownElement = (props: any) => {
const { node, children } = props;
const content = stringifyContent(children);
const { node, children } = props
const content = stringifyContent(children)
return (
<div className="relative group">
@ -59,22 +65,30 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
{React.createElement(node.tagName, {
...props,
className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors`
}, children)}
{React.createElement(
node.tagName,
{
...props,
className: `${
props.className || ""
} hover:bg-transparent rounded p-1 transition-colors`,
},
children
)}
</div>
);
};
)
}
return (
<div className="text-left relative">
<div className={`relative p-2 rounded-lg ${
message.role === 'user'
? 'bg-[#262626] text-white'
: 'bg-transparent text-white'
} max-w-full`}>
{message.role === 'user' && (
<div
className={`relative p-2 rounded-lg ${
message.role === "user"
? "bg-[#262626] text-white"
: "bg-transparent text-white"
} max-w-full`}
>
{message.role === "user" && (
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(message.content)}
<Button
@ -89,13 +103,13 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
)}
{message.context && (
<div className="mb-2 bg-input rounded-lg">
<div
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
onClick={() =>
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
}
>
<span className="text-sm text-gray-300">
Context
</span>
<span className="text-sm text-gray-300">Context</span>
{expandedMessageIndex === 0 ? (
<ChevronUp size={16} />
) : (
@ -105,41 +119,46 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
{expandedMessageIndex === 0 && (
<div className="relative">
<div className="absolute top-0 right-0 flex p-1">
{renderCopyButton(message.context.replace(/^Regarding this code:\n/, ''))}
{renderCopyButton(
message.context.replace(/^Regarding this code:\n/, "")
)}
</div>
{(() => {
const code = message.context.replace(/^Regarding this code:\n/, '');
const match = /language-(\w+)/.exec(code);
const language = match ? match[1] : 'typescript';
const code = message.context.replace(
/^Regarding this code:\n/,
""
)
const match = /language-(\w+)/.exec(code)
const language = match ? match[1] : "typescript"
return (
<div className="pt-6">
<textarea
value={code}
onChange={(e) => {
const updatedContext = `Regarding this code:\n${e.target.value}`;
setContext(updatedContext);
const updatedContext = `Regarding this code:\n${e.target.value}`
setContext(updatedContext)
}}
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
rows={code.split('\n').length}
rows={code.split("\n").length}
style={{
resize: 'vertical',
minHeight: '100px',
maxHeight: '400px',
resize: "vertical",
minHeight: "100px",
maxHeight: "400px",
}}
/>
</div>
);
)
})()}
</div>
)}
</div>
)}
{message.role === 'assistant' ? (
{message.role === "assistant" ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({node, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '');
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "")
return match ? (
<div className="relative border border-input rounded-md my-4">
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
@ -163,8 +182,8 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
PreTag="div"
customStyle={{
margin: 0,
padding: '0.5rem',
fontSize: '0.875rem',
padding: "0.5rem",
fontSize: "0.875rem",
}}
>
{stringifyContent(children)}
@ -175,7 +194,7 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
<code className={className} {...props}>
{children}
</code>
);
)
},
p: renderMarkdownElement,
h1: renderMarkdownElement,
@ -184,18 +203,24 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
h4: renderMarkdownElement,
h5: renderMarkdownElement,
h6: renderMarkdownElement,
ul: (props) => <ul className="list-disc pl-6 mb-4 space-y-2">{props.children}</ul>,
ol: (props) => <ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>,
ul: (props) => (
<ul className="list-disc pl-6 mb-4 space-y-2">
{props.children}
</ul>
),
ol: (props) => (
<ol className="list-decimal pl-6 mb-4 space-y-2">
{props.children}
</ol>
),
}}
>
{message.content}
</ReactMarkdown>
) : (
<div className="whitespace-pre-wrap group">
{message.content}
</div>
<div className="whitespace-pre-wrap group">{message.content}</div>
)}
</div>
</div>
);
)
}

View File

@ -1,48 +1,60 @@
import React from 'react';
import { ChevronUp, ChevronDown, X } from 'lucide-react';
import { ChevronDown, ChevronUp, X } from "lucide-react"
interface ContextDisplayProps {
context: string | null;
isContextExpanded: boolean;
setIsContextExpanded: (isExpanded: boolean) => void;
setContext: (context: string | null) => void;
context: string | null
isContextExpanded: boolean
setIsContextExpanded: (isExpanded: boolean) => void
setContext: (context: string | null) => void
}
export default function ContextDisplay({ context, isContextExpanded, setIsContextExpanded, setContext }: ContextDisplayProps) {
if (!context) return null;
export default function ContextDisplay({
context,
isContextExpanded,
setIsContextExpanded,
setContext,
}: ContextDisplayProps) {
if (!context) return null
return (
<div className="mb-2 bg-input p-2 rounded-lg">
<div className="flex justify-between items-center">
<div
className="flex-grow cursor-pointer"
<div
className="flex-grow cursor-pointer"
onClick={() => setIsContextExpanded(!isContextExpanded)}
>
<span className="text-sm text-gray-300">
Context
</span>
<span className="text-sm text-gray-300">Context</span>
</div>
<div className="flex items-center">
{isContextExpanded ? (
<ChevronUp size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(false)} />
<ChevronUp
size={16}
className="cursor-pointer"
onClick={() => setIsContextExpanded(false)}
/>
) : (
<ChevronDown size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(true)} />
<ChevronDown
size={16}
className="cursor-pointer"
onClick={() => setIsContextExpanded(true)}
/>
)}
<X
size={16}
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
<X
size={16}
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
onClick={() => setContext(null)}
/>
</div>
</div>
{isContextExpanded && (
<textarea
value={context.replace(/^Regarding this code:\n/, '')}
onChange={(e) => setContext(`Regarding this code:\n${e.target.value}`)}
value={context.replace(/^Regarding this code:\n/, "")}
onChange={(e) =>
setContext(`Regarding this code:\n${e.target.value}`)
}
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
rows={5}
/>
)}
</div>
);
)
}

View File

@ -1,52 +1,76 @@
import React, { useState, useEffect, useRef } from 'react';
import LoadingDots from '../../ui/LoadingDots';
import ChatMessage from './ChatMessage';
import ChatInput from './ChatInput';
import ContextDisplay from './ContextDisplay';
import { handleSend, handleStopGeneration } from './lib/chatUtils';
import { X } from "lucide-react"
import { useEffect, useRef, useState } from "react"
import LoadingDots from "../../ui/LoadingDots"
import ChatInput from "./ChatInput"
import ChatMessage from "./ChatMessage"
import ContextDisplay from "./ContextDisplay"
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
interface Message {
role: 'user' | 'assistant';
content: string;
context?: string;
role: "user" | "assistant"
content: string
context?: string
}
export default function AIChat({ activeFileContent, activeFileName }: { activeFileContent: string, activeFileName: string }) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const chatContainerRef = useRef<HTMLDivElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const [context, setContext] = useState<string | null>(null);
const [isContextExpanded, setIsContextExpanded] = useState(false);
const [isLoading, setIsLoading] = useState(false);
export default function AIChat({
activeFileContent,
activeFileName,
onClose,
}: {
activeFileContent: string
activeFileName: string
onClose: () => void
}) {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState("")
const [isGenerating, setIsGenerating] = useState(false)
const chatContainerRef = useRef<HTMLDivElement>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const [context, setContext] = useState<string | null>(null)
const [isContextExpanded, setIsContextExpanded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
scrollToBottom();
}, [messages]);
scrollToBottom()
}, [messages])
const scrollToBottom = () => {
if (chatContainerRef.current) {
setTimeout(() => {
chatContainerRef.current?.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: 'smooth'
});
}, 100);
behavior: "smooth",
})
}, 100)
}
};
}
return (
<div className="flex flex-col h-screen w-full">
<div className="flex justify-between items-center p-2 border-b">
<span className="text-muted-foreground/50 font-medium">CHAT</span>
<span className="text-muted-foreground/50 font-medium truncate max-w-[50%]" title={activeFileName}>{activeFileName}</span>
<div className="flex items-center h-full">
<span className="text-muted-foreground/50 font-medium">
{activeFileName}
</span>
<div className="mx-2 h-full w-px bg-muted-foreground/20"></div>
<button
onClick={onClose}
className="text-muted-foreground/50 hover:text-muted-foreground focus:outline-none"
aria-label="Close AI Chat"
>
<X size={18} />
</button>
</div>
</div>
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4">
<div
ref={chatContainerRef}
className="flex-grow overflow-y-auto p-4 space-y-4"
>
{messages.map((message, messageIndex) => (
<ChatMessage
key={messageIndex}
message={message}
<ChatMessage
key={messageIndex}
message={message}
setContext={setContext}
setIsContextExpanded={setIsContextExpanded}
/>
@ -54,20 +78,33 @@ export default function AIChat({ activeFileContent, activeFileName }: { activeFi
{isLoading && <LoadingDots />}
</div>
<div className="p-4 border-t mb-14">
<ContextDisplay
context={context}
<ContextDisplay
context={context}
isContextExpanded={isContextExpanded}
setIsContextExpanded={setIsContextExpanded}
setContext={setContext}
/>
<ChatInput
<ChatInput
input={input}
setInput={setInput}
isGenerating={isGenerating}
handleSend={() => handleSend(input, context, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent)}
handleSend={() =>
handleSend(
input,
context,
messages,
setMessages,
setInput,
setIsContextExpanded,
setIsGenerating,
setIsLoading,
abortControllerRef,
activeFileContent
)
}
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
/>
</div>
</div>
);
)
}

View File

@ -1,58 +1,68 @@
import React from 'react';
import React from "react"
export const stringifyContent = (content: any, seen = new WeakSet()): string => {
if (typeof content === 'string') {
return content;
export const stringifyContent = (
content: any,
seen = new WeakSet()
): string => {
if (typeof content === "string") {
return content
}
if (content === null) {
return 'null';
return "null"
}
if (content === undefined) {
return 'undefined';
return "undefined"
}
if (typeof content === 'number' || typeof content === 'boolean') {
return content.toString();
if (typeof content === "number" || typeof content === "boolean") {
return content.toString()
}
if (typeof content === 'function') {
return content.toString();
if (typeof content === "function") {
return content.toString()
}
if (typeof content === 'symbol') {
return content.toString();
if (typeof content === "symbol") {
return content.toString()
}
if (typeof content === 'bigint') {
return content.toString() + 'n';
if (typeof content === "bigint") {
return content.toString() + "n"
}
if (React.isValidElement(content)) {
return React.Children.toArray((content as React.ReactElement).props.children)
.map(child => stringifyContent(child, seen))
.join('');
return React.Children.toArray(
(content as React.ReactElement).props.children
)
.map((child) => stringifyContent(child, seen))
.join("")
}
if (Array.isArray(content)) {
return '[' + content.map(item => stringifyContent(item, seen)).join(', ') + ']';
return (
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
)
}
if (typeof content === 'object') {
if (typeof content === "object") {
if (seen.has(content)) {
return '[Circular]';
return "[Circular]"
}
seen.add(content);
seen.add(content)
try {
const pairs = Object.entries(content).map(
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
);
return '{' + pairs.join(', ') + '}';
)
return "{" + pairs.join(", ") + "}"
} catch (error) {
return Object.prototype.toString.call(content);
return Object.prototype.toString.call(content)
}
}
return String(content);
};
return String(content)
}
export const copyToClipboard = (text: string, setCopiedText: (text: string | null) => void) => {
export const copyToClipboard = (
text: string,
setCopiedText: (text: string | null) => void
) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedText(text);
setTimeout(() => setCopiedText(null), 2000);
});
};
setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000)
})
}
export const handleSend = async (
input: string,
@ -66,97 +76,105 @@ export const handleSend = async (
abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string
) => {
if (input.trim() === '' && !context) return;
if (input.trim() === "" && !context) return
const newMessage = {
role: 'user' as const,
const newMessage = {
role: "user" as const,
content: input,
context: context || undefined
};
const updatedMessages = [...messages, newMessage];
setMessages(updatedMessages);
setInput('');
setIsContextExpanded(false);
setIsGenerating(true);
setIsLoading(true);
context: context || undefined,
}
const updatedMessages = [...messages, newMessage]
setMessages(updatedMessages)
setInput("")
setIsContextExpanded(false)
setIsGenerating(true)
setIsLoading(true)
abortControllerRef.current = new AbortController();
abortControllerRef.current = new AbortController()
try {
const anthropicMessages = updatedMessages.map(msg => ({
role: msg.role === 'user' ? 'human' : 'assistant',
content: msg.content
}));
const anthropicMessages = updatedMessages.map((msg) => ({
role: msg.role === "user" ? "human" : "assistant",
content: msg.content,
}))
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: anthropicMessages,
context: context || undefined,
activeFileContent: activeFileContent,
}),
signal: abortControllerRef.current.signal,
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: anthropicMessages,
context: context || undefined,
activeFileContent: activeFileContent,
}),
signal: abortControllerRef.current.signal,
}
)
if (!response.ok) {
throw new Error('Failed to get AI response');
throw new Error("Failed to get AI response")
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
const assistantMessage = { role: 'assistant' as const, content: '' };
setMessages([...updatedMessages, assistantMessage]);
setIsLoading(false);
const reader = response.body?.getReader()
const decoder = new TextDecoder()
const assistantMessage = { role: "assistant" as const, content: "" }
setMessages([...updatedMessages, assistantMessage])
setIsLoading(false)
let buffer = '';
const updateInterval = 100;
let lastUpdateTime = Date.now();
let buffer = ""
const updateInterval = 100
let lastUpdateTime = Date.now()
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const currentTime = Date.now();
const currentTime = Date.now()
if (currentTime - lastUpdateTime > updateInterval) {
setMessages(prev => {
const updatedMessages = [...prev];
const lastMessage = updatedMessages[updatedMessages.length - 1];
lastMessage.content = buffer;
return updatedMessages;
});
lastUpdateTime = currentTime;
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer
return updatedMessages
})
lastUpdateTime = currentTime
}
}
setMessages(prev => {
const updatedMessages = [...prev];
const lastMessage = updatedMessages[updatedMessages.length - 1];
lastMessage.content = buffer;
return updatedMessages;
});
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer
return updatedMessages
})
}
} catch (error: any) {
if (error.name === 'AbortError') {
console.log('Generation aborted');
if (error.name === "AbortError") {
console.log("Generation aborted")
} else {
console.error('Error fetching AI response:', error);
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' };
setMessages(prev => [...prev, errorMessage]);
console.error("Error fetching AI response:", error)
const errorMessage = {
role: "assistant" as const,
content: "Sorry, I encountered an error. Please try again.",
}
setMessages((prev) => [...prev, errorMessage])
}
} finally {
setIsGenerating(false);
setIsLoading(false);
abortControllerRef.current = null;
setIsGenerating(false)
setIsLoading(false)
abortControllerRef.current = null
}
};
}
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => {
export const handleStopGeneration = (
abortControllerRef: React.MutableRefObject<AbortController | null>
) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current.abort()
}
};
}

View File

@ -1,13 +1,13 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client"
import { Editor } from "@monaco-editor/react"
import { User } from "@/lib/types"
import { toast } from "sonner"
import { Editor } from "@monaco-editor/react"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { usePathname, useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { Socket } from "socket.io-client"
import { toast } from "sonner"
import { Button } from "../ui/button"
// import monaco from "monaco-editor"
export default function GenerateInput({

View File

@ -91,6 +91,7 @@ export default function CodeEditor({
// Layout state
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
const [previousLayout, setPreviousLayout] = useState(false)
// AI Chat state
const [isAIChatOpen, setIsAIChatOpen] = useState(false)
@ -548,12 +549,18 @@ export default function CodeEditor({
setIsAIChatOpen((prev) => !prev)
}
}
document.addEventListener("keydown", down)
// Added this line to prevent Monaco editor from handling Cmd/Ctrl+L
editorRef?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, () => {
setIsAIChatOpen((prev) => !prev)
})
return () => {
document.removeEventListener("keydown", down)
}
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen])
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
// Liveblocks live collaboration setup effect
useEffect(() => {
@ -868,7 +875,24 @@ export default function CodeEditor({
}
const toggleLayout = () => {
setIsHorizontalLayout((prev) => !prev)
if (!isAIChatOpen) {
setIsHorizontalLayout((prev) => !prev)
}
}
// Add an effect to handle layout changes when AI chat is opened/closed
useEffect(() => {
if (isAIChatOpen) {
setPreviousLayout(isHorizontalLayout)
setIsHorizontalLayout(true)
} else {
setIsHorizontalLayout(previousLayout)
}
}, [isAIChatOpen])
// Modify the toggleAIChat function
const toggleAIChat = () => {
setIsAIChatOpen((prev) => !prev)
}
// On disabled access for shared users, show un-interactable loading placeholder + info modal
@ -1007,7 +1031,9 @@ export default function CodeEditor({
deletingFolderId={deletingFolderId}
/>
{/* Outer ResizablePanelGroup for main layout */}
<ResizablePanelGroup direction="horizontal">
<ResizablePanelGroup
direction={isHorizontalLayout ? "horizontal" : "vertical"}
>
{/* Left side: Editor and Preview/Terminal */}
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
<ResizablePanelGroup
@ -1136,6 +1162,7 @@ export default function CodeEditor({
size="sm"
variant="ghost"
className="mr-2 border"
disabled={isAIChatOpen}
>
{isHorizontalLayout ? (
<ArrowRightToLine className="w-4 h-4" />
@ -1190,6 +1217,7 @@ export default function CodeEditor({
tabs.find((tab) => tab.id === activeFileId)?.name ||
"No file selected"
}
onClose={toggleAIChat}
/>
</ResizablePanel>
</>

View File

@ -1,6 +1,6 @@
"use client";
"use client"
import { useOthers } from "@/liveblocks.config";
import { useOthers } from "@/liveblocks.config"
const classNames = {
red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium",
@ -14,10 +14,10 @@ const classNames = {
purple:
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium",
pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium",
};
}
export function Avatars() {
const users = useOthers();
const users = useOthers()
return (
<>
@ -30,12 +30,12 @@ export function Avatars() {
.slice(0, 2)
.map((letter) => letter[0].toUpperCase())}
</div>
);
)
})}
</div>
{users.length > 0 ? (
<div className="h-full w-[1px] bg-border mx-2" />
) : null}
</>
);
)
}

View File

@ -1,11 +1,10 @@
import { useEffect, useMemo, useState } from "react"
import { colors } from "@/lib/colors"
import {
AwarenessList,
TypedLiveblocksProvider,
UserAwareness,
useSelf,
} from "@/liveblocks.config"
import { colors } from "@/lib/colors"
import { useEffect, useMemo, useState } from "react"
export function Cursors({
yProvider,

View File

@ -1,43 +1,35 @@
"use client";
"use client"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
} from "@/components/ui/dialog"
import {
ChevronRight,
FileStack,
Globe,
Loader2,
TextCursor,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { Loader2 } from "lucide-react"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
export default function DisableAccessModal({
open,
setOpen,
message,
}: {
open: boolean;
setOpen: (open: boolean) => void;
message: string;
open: boolean
setOpen: (open: boolean) => void
message: string
}) {
const router = useRouter();
const router = useRouter()
useEffect(() => {
if (open) {
const timeout = setTimeout(() => {
router.push("/dashboard");
}, 5000);
return () => clearTimeout(timeout);
router.push("/dashboard")
}, 5000)
return () => clearTimeout(timeout)
}
}, []);
}, [])
return (
<Dialog open={open} onOpenChange={setOpen}>
@ -54,5 +46,5 @@ export default function DisableAccessModal({
</div>
</DialogContent>
</Dialog>
);
)
}

View File

@ -1,14 +1,13 @@
"use client";
"use client"
import { RoomProvider } from "@/liveblocks.config";
import { ClientSideSuspense } from "@liveblocks/react";
import { RoomProvider } from "@/liveblocks.config"
export function Room({
id,
children,
}: {
id: string;
children: React.ReactNode;
id: string
children: React.ReactNode
}) {
return (
<RoomProvider
@ -21,5 +20,5 @@ export function Room({
{children}
{/* </ClientSideSuspense> */}
</RoomProvider>
);
)
}

View File

@ -1,9 +1,6 @@
"use client"
import Image from "next/image"
import Logo from "@/assets/logo.svg"
import { Skeleton } from "@/components/ui/skeleton"
import { Loader2, X } from "lucide-react"
import {
Dialog,
DialogContent,
@ -11,6 +8,9 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Skeleton } from "@/components/ui/skeleton"
import { Loader2, X } from "lucide-react"
import Image from "next/image"
import { useEffect, useState } from "react"
export default function Loading({

View File

@ -1,34 +1,38 @@
"use client";
"use client"
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useTerminal } from "@/context/TerminalContext";
import { Play, Pause, Globe, Globe2 } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Sandbox, User } from "@/lib/types";
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { useTerminal } from "@/context/TerminalContext"
import { Sandbox, User } from "@/lib/types"
import { Globe } from "lucide-react"
import { useState } from "react"
export default function DeployButtonModal({
userData,
data,
}: {
userData: User;
data: Sandbox;
userData: User
data: Sandbox
}) {
const { deploy } = useTerminal();
const [isDeploying, setIsDeploying] = useState(false);
const { deploy } = useTerminal()
const [isDeploying, setIsDeploying] = useState(false)
const handleDeploy = () => {
if (isDeploying) {
console.log("Stopping deployment...");
setIsDeploying(false);
console.log("Stopping deployment...")
setIsDeploying(false)
} else {
console.log("Starting deployment...");
setIsDeploying(true);
console.log("Starting deployment...")
setIsDeploying(true)
deploy(() => {
setIsDeploying(false);
});
setIsDeploying(false)
})
}
};
}
return (
<>
@ -39,7 +43,10 @@ export default function DeployButtonModal({
Deploy
</Button>
</PopoverTrigger>
<PopoverContent className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg" style={{ backgroundColor: 'rgb(10,10,10)', color: 'white' }}>
<PopoverContent
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
style={{ backgroundColor: "rgb(10,10,10)", color: "white" }}
>
<h3 className="font-semibold text-gray-300 mb-2">Domains</h3>
<div className="flex flex-col gap-4">
<DeploymentOption
@ -49,16 +56,30 @@ export default function DeployButtonModal({
user={userData.name}
/>
</div>
<Button variant="outline" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" onClick={handleDeploy}>
{isDeploying ? "Deploying..." : "Update"}
<Button
variant="outline"
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
onClick={handleDeploy}
>
{isDeploying ? "Deploying..." : "Update"}
</Button>
</PopoverContent>
</Popover>
</>
);
)
}
function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.ReactNode; domain: string; timestamp: string; user: string }) {
function DeploymentOption({
icon,
domain,
timestamp,
user,
}: {
icon: React.ReactNode
domain: string
timestamp: string
user: string
}) {
return (
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
<div className="flex items-start gap-2 relative">
@ -72,7 +93,9 @@ function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.React
{domain}
</a>
</div>
<p className="text-sm text-gray-400 mt-0 ml-7">{timestamp} {user}</p>
<p className="text-sm text-gray-400 mt-0 ml-7">
{timestamp} {user}
</p>
</div>
);
)
}

View File

@ -1,60 +1,57 @@
"use client";
"use client"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
} from "@/components/ui/dialog"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { Sandbox } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { deleteSandbox, updateSandbox } from "@/lib/actions";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
} from "@/components/ui/select"
import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { Sandbox } from "@/lib/types"
import { Loader2 } from "lucide-react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
const formSchema = z.object({
name: z.string().min(1).max(16),
visibility: z.enum(["public", "private"]),
});
})
export default function EditSandboxModal({
open,
setOpen,
data,
}: {
open: boolean;
setOpen: (open: boolean) => void;
data: Sandbox;
open: boolean
setOpen: (open: boolean) => void
data: Sandbox
}) {
const [loading, setLoading] = useState(false);
const [loadingDelete, setLoadingDelete] = useState(false);
const [loading, setLoading] = useState(false)
const [loadingDelete, setLoadingDelete] = useState(false)
const router = useRouter();
const router = useRouter()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@ -62,22 +59,22 @@ export default function EditSandboxModal({
name: data.name,
visibility: data.visibility,
},
});
})
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true);
await updateSandbox({ id: data.id, ...values });
setLoading(true)
await updateSandbox({ id: data.id, ...values })
toast.success("Sandbox updated successfully");
toast.success("Sandbox updated successfully")
setLoading(false);
setLoading(false)
}
async function onDelete() {
setLoadingDelete(true);
await deleteSandbox(data.id);
setLoadingDelete(true)
await deleteSandbox(data.id)
router.push("/dashboard");
router.push("/dashboard")
}
return (
@ -153,5 +150,5 @@ export default function EditSandboxModal({
</Button>
</DialogContent>
</Dialog>
);
)
}

View File

@ -1,73 +1,78 @@
"use client";
"use client"
import React, { useEffect, useRef } from 'react';
import { Play, StopCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTerminal } from "@/context/TerminalContext";
import { usePreview } from "@/context/PreviewContext";
import { toast } from "sonner";
import { Sandbox } from "@/lib/types";
import { Button } from "@/components/ui/button"
import { usePreview } from "@/context/PreviewContext"
import { useTerminal } from "@/context/TerminalContext"
import { Sandbox } from "@/lib/types"
import { Play, StopCircle } from "lucide-react"
import { useEffect, useRef } from "react"
import { toast } from "sonner"
export default function RunButtonModal({
isRunning,
setIsRunning,
sandboxData,
}: {
isRunning: boolean;
setIsRunning: (running: boolean) => void;
sandboxData: Sandbox;
isRunning: boolean
setIsRunning: (running: boolean) => void
sandboxData: Sandbox
}) {
const { createNewTerminal, closeTerminal, terminals } = useTerminal();
const { setIsPreviewCollapsed, previewPanelRef } = usePreview();
const { createNewTerminal, closeTerminal, terminals } = useTerminal()
const { setIsPreviewCollapsed, previewPanelRef } = usePreview()
// Ref to keep track of the last created terminal's ID
const lastCreatedTerminalRef = useRef<string | null>(null);
const lastCreatedTerminalRef = useRef<string | null>(null)
// Effect to update the lastCreatedTerminalRef when a new terminal is added
useEffect(() => {
if (terminals.length > 0 && !isRunning) {
const latestTerminal = terminals[terminals.length - 1];
if (latestTerminal && latestTerminal.id !== lastCreatedTerminalRef.current) {
lastCreatedTerminalRef.current = latestTerminal.id;
const latestTerminal = terminals[terminals.length - 1]
if (
latestTerminal &&
latestTerminal.id !== lastCreatedTerminalRef.current
) {
lastCreatedTerminalRef.current = latestTerminal.id
}
}
}, [terminals, isRunning]);
}, [terminals, isRunning])
const handleRun = async () => {
if (isRunning && lastCreatedTerminalRef.current)
{
await closeTerminal(lastCreatedTerminalRef.current);
lastCreatedTerminalRef.current = null;
setIsPreviewCollapsed(true);
previewPanelRef.current?.collapse();
}
else if (!isRunning && terminals.length < 4)
{
const command = sandboxData.type === "streamlit"
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
: "yarn install && yarn dev";
if (isRunning && lastCreatedTerminalRef.current) {
await closeTerminal(lastCreatedTerminalRef.current)
lastCreatedTerminalRef.current = null
setIsPreviewCollapsed(true)
previewPanelRef.current?.collapse()
} else if (!isRunning && terminals.length < 4) {
const command =
sandboxData.type === "streamlit"
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
: "yarn install && yarn dev"
try {
// Create a new terminal with the appropriate command
await createNewTerminal(command);
setIsPreviewCollapsed(false);
previewPanelRef.current?.expand();
await createNewTerminal(command)
setIsPreviewCollapsed(false)
previewPanelRef.current?.expand()
} catch (error) {
toast.error("Failed to create new terminal.");
console.error("Error creating new terminal:", error);
return;
toast.error("Failed to create new terminal.")
console.error("Error creating new terminal:", error)
return
}
} else if (!isRunning) {
toast.error("You've reached the maximum number of terminals.");
return;
toast.error("You've reached the maximum number of terminals.")
return
}
setIsRunning(!isRunning);
};
setIsRunning(!isRunning)
}
return (
<Button variant="outline" onClick={handleRun}>
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
{isRunning ? 'Stop' : 'Run'}
{isRunning ? (
<StopCircle className="w-4 h-4 mr-2" />
) : (
<Play className="w-4 h-4 mr-2" />
)}
{isRunning ? "Stop" : "Run"}
</Button>
);
}
)
}

View File

@ -6,10 +6,11 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
@ -18,14 +19,13 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Link, Loader2, UserPlus, X } from "lucide-react"
import { useState } from "react"
import { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button"
import { shareSandbox } from "@/lib/actions"
import { Sandbox } from "@/lib/types"
import { DialogDescription } from "@radix-ui/react-dialog"
import { Link, Loader2, UserPlus } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner"
import SharedUser from "./sharedUser"
import { DialogDescription } from "@radix-ui/react-dialog"
const formSchema = z.object({
email: z.string().email(),

View File

@ -1,66 +1,69 @@
"use client"
import { Link, RotateCw, UnfoldVertical } from "lucide-react"
import {
Link,
RotateCw,
TerminalSquare,
UnfoldVertical,
} from "lucide-react"
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react"
import { toast } from "sonner"
export default forwardRef(function PreviewWindow({
collapsed,
open,
src
}: {
collapsed: boolean
open: () => void
src: string
},
ref: React.Ref<{
refreshIframe: () => void
}>) {
export default forwardRef(function PreviewWindow(
{
collapsed,
open,
src,
}: {
collapsed: boolean
open: () => void
src: string
},
ref: React.Ref<{
refreshIframe: () => void
}>
) {
const frameRef = useRef<HTMLIFrameElement>(null)
const [iframeKey, setIframeKey] = useState(0)
const refreshIframe = () => {
setIframeKey(prev => prev + 1)
setIframeKey((prev) => prev + 1)
}
// Refresh the preview when the URL changes.
// Refresh the preview when the URL changes.
useEffect(refreshIframe, [src])
// Expose refreshIframe method to the parent.
useImperativeHandle(ref, () => ({ refreshIframe }))
return (
<>
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
<div className="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1">
{collapsed ? (
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
<div className="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1">
{collapsed ? (
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton>
) : (
<>
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton>
) : (
<>
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton>
<PreviewButton
onClick={() => {
navigator.clipboard.writeText(src)
toast.info("Copied preview link to clipboard")
}}
>
<Link className="w-4 h-4" />
</PreviewButton>
<PreviewButton onClick={refreshIframe}>
<RotateCw className="w-3 h-3" />
</PreviewButton>
</>
)}
</div>
<PreviewButton
onClick={() => {
navigator.clipboard.writeText(src)
toast.info("Copied preview link to clipboard")
}}
>
<Link className="w-4 h-4" />
</PreviewButton>
<PreviewButton onClick={refreshIframe}>
<RotateCw className="w-3 h-3" />
</PreviewButton>
</>
)}
</div>
</div>
</>
)
})
@ -76,8 +79,9 @@ function PreviewButton({
}) {
return (
<div
className={`${disabled ? "pointer-events-none opacity-50" : ""
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
className={`${
disabled ? "pointer-events-none opacity-50" : ""
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
onClick={onClick}
>
{children}

View File

@ -1,18 +1,18 @@
"use client";
"use client"
import Image from "next/image";
import { getIconForFile } from "vscode-icons-js";
import { TFile, TTab } from "@/lib/types";
import { useEffect, useRef, useState } from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Loader2, Pencil, Trash2 } from "lucide-react";
} from "@/components/ui/context-menu"
import { TFile, TTab } from "@/lib/types"
import { Loader2, Pencil, Trash2 } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFile } from "vscode-icons-js"
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
export default function SidebarFile({
data,
@ -22,36 +22,36 @@ export default function SidebarFile({
movingId,
deletingFolderId,
}: {
data: TFile;
selectFile: (file: TTab) => void;
data: TFile
selectFile: (file: TTab) => void
handleRename: (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => boolean;
handleDeleteFile: (file: TFile) => void;
movingId: string;
deletingFolderId: string;
) => boolean
handleDeleteFile: (file: TFile) => void
movingId: string
deletingFolderId: string
}) {
const isMoving = movingId === data.id;
const isMoving = movingId === data.id
const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId)
const ref = useRef(null); // for draggable
const [dragging, setDragging] = useState(false);
const ref = useRef(null) // for draggable
const [dragging, setDragging] = useState(false)
const inputRef = useRef<HTMLInputElement>(null);
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`);
const [editing, setEditing] = useState(false);
const [pendingDelete, setPendingDelete] = useState(isDeleting);
const inputRef = useRef<HTMLInputElement>(null)
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`)
const [editing, setEditing] = useState(false)
const [pendingDelete, setPendingDelete] = useState(isDeleting)
useEffect(() => {
setPendingDelete(isDeleting);
}, [isDeleting]);
setPendingDelete(isDeleting)
}, [isDeleting])
useEffect(() => {
const el = ref.current;
const el = ref.current
if (el)
return draggable({
@ -59,14 +59,14 @@ export default function SidebarFile({
onDragStart: () => setDragging(true),
onDrop: () => setDragging(false),
getInitialData: () => ({ id: data.id }),
});
}, []);
})
}, [])
useEffect(() => {
if (editing) {
setTimeout(() => inputRef.current?.focus(), 0);
setTimeout(() => inputRef.current?.focus(), 0)
}
}, [editing, inputRef.current]);
}, [editing, inputRef.current])
const renameFile = () => {
const renamed = handleRename(
@ -74,12 +74,12 @@ export default function SidebarFile({
inputRef.current?.value ?? data.name,
data.name,
"file"
);
)
if (!renamed && inputRef.current) {
inputRef.current.value = data.name;
inputRef.current.value = data.name
}
setEditing(false);
};
setEditing(false)
}
return (
<ContextMenu>
@ -88,7 +88,7 @@ export default function SidebarFile({
disabled={pendingDelete || dragging || isMoving}
onClick={() => {
if (!editing && !pendingDelete && !isMoving)
selectFile({ ...data, saved: true });
selectFile({ ...data, saved: true })
}}
onDoubleClick={() => {
setEditing(true)
@ -119,8 +119,8 @@ export default function SidebarFile({
) : (
<form
onSubmit={(e) => {
e.preventDefault();
renameFile();
e.preventDefault()
renameFile()
}}
>
<input
@ -138,8 +138,8 @@ export default function SidebarFile({
<ContextMenuContent>
<ContextMenuItem
onClick={() => {
console.log("rename");
setEditing(true);
console.log("rename")
setEditing(true)
}}
>
<Pencil className="w-4 h-4 mr-2" />
@ -148,9 +148,9 @@ export default function SidebarFile({
<ContextMenuItem
disabled={pendingDelete}
onClick={() => {
console.log("delete");
setPendingDelete(true);
handleDeleteFile(data);
console.log("delete")
setPendingDelete(true)
handleDeleteFile(data)
}}
>
<Trash2 className="w-4 h-4 mr-2" />
@ -158,5 +158,5 @@ export default function SidebarFile({
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
)
}

View File

@ -1,20 +1,20 @@
"use client"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import { TFile, TFolder, TTab } from "@/lib/types"
import SidebarFile from "./file"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react"
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { TFile, TFolder, TTab } from "@/lib/types"
import { cn } from "@/lib/utils"
import { motion, AnimatePresence } from "framer-motion"
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { AnimatePresence, motion } from "framer-motion"
import { ChevronRight, Pencil, Trash2 } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import SidebarFile from "./file"
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out

View File

@ -1,28 +1,20 @@
"use client"
import {
FilePlus,
FolderPlus,
Loader2,
MonitorPlay,
Search,
Sparkles,
} from "lucide-react"
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
import { FilePlus, FolderPlus, MessageSquareMore, Sparkles } from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import { Socket } from "socket.io-client"
import SidebarFile from "./file"
import SidebarFolder from "./folder"
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
import { useEffect, useMemo, useRef, useState } from "react"
import New from "./new"
import { Socket } from "socket.io-client"
import { Switch } from "@/components/ui/switch"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { sortFileExplorer } from "@/lib/utils"
import {
dropTargetForElements,
monitorForElements,
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import Button from "@/components/ui/customButton"
import { Skeleton } from "@/components/ui/skeleton"
import { sortFileExplorer } from "@/lib/utils"
export default function Sidebar({
sandboxData,
@ -107,9 +99,9 @@ export default function Sidebar({
}, [])
return (
<div className="h-full w-56 select-none flex flex-col text-sm items-start justify-between p-2">
<div className="w-full flex flex-col items-start">
<div className="flex w-full items-center justify-between h-8 mb-1 ">
<div className="h-full w-56 select-none flex flex-col text-sm">
<div className="flex-grow overflow-auto p-2 pb-[84px]">
<div className="flex w-full items-center justify-between h-8 mb-1">
<div className="text-muted-foreground">Explorer</div>
<div className="flex space-x-1">
<button
@ -185,10 +177,37 @@ export default function Sidebar({
)}
</div>
</div>
<div className="w-full space-y-4">
{/* <Button className="w-full">
<MonitorPlay className="w-4 h-4 mr-2" /> Run
</Button> */}
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
<Button
variant="ghost"
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
disabled
aria-disabled="true"
style={{ opacity: 1 }}
>
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
Copilot
<div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
<span className="text-xs"></span>G
</kbd>
</div>
</Button>
<Button
variant="ghost"
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
disabled
aria-disabled="true"
style={{ opacity: 1 }}
>
<MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
AI Chat
<div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
<span className="text-xs"></span>L
</kbd>
</div>
</Button>
</div>
</div>
)

View File

@ -1,9 +1,9 @@
"use client";
"use client"
import { validateName } from "@/lib/utils";
import Image from "next/image";
import { useEffect, useRef } from "react";
import { Socket } from "socket.io-client";
import { validateName } from "@/lib/utils"
import Image from "next/image"
import { useEffect, useRef } from "react"
import { Socket } from "socket.io-client"
export default function New({
socket,
@ -11,18 +11,18 @@ export default function New({
stopEditing,
addNew,
}: {
socket: Socket;
type: "file" | "folder";
stopEditing: () => void;
addNew: (name: string, type: "file" | "folder") => void;
socket: Socket
type: "file" | "folder"
stopEditing: () => void
addNew: (name: string, type: "file" | "folder") => void
}) {
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLInputElement>(null)
function createNew() {
const name = inputRef.current?.value;
const name = inputRef.current?.value
if (name) {
const valid = validateName(name, "", type);
const valid = validateName(name, "", type)
if (valid.status) {
if (type === "file") {
socket.emit(
@ -30,23 +30,23 @@ export default function New({
name,
({ success }: { success: boolean }) => {
if (success) {
addNew(name, type);
addNew(name, type)
}
}
);
)
} else {
socket.emit("createFolder", name, () => {
addNew(name, type);
});
addNew(name, type)
})
}
}
}
stopEditing();
stopEditing()
}
useEffect(() => {
inputRef.current?.focus();
}, []);
inputRef.current?.focus()
}, [])
return (
<div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
@ -63,8 +63,8 @@ export default function New({
/>
<form
onSubmit={(e) => {
e.preventDefault();
createNew();
e.preventDefault()
createNew()
}}
>
<input
@ -74,5 +74,5 @@ export default function New({
/>
</form>
</div>
);
)
}

View File

@ -1,18 +1,17 @@
"use client";
"use client"
import { Button } from "@/components/ui/button";
import Tab from "@/components/ui/tab";
import { Terminal } from "@xterm/xterm";
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
import { toast } from "sonner";
import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
import { Button } from "@/components/ui/button"
import Tab from "@/components/ui/tab"
import { useSocket } from "@/context/SocketContext"
import { useTerminal } from "@/context/TerminalContext"
import { Terminal } from "@xterm/xterm"
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"
import { useEffect } from "react"
import { toast } from "sonner"
import EditorTerminal from "./terminal"
export default function Terminals() {
const { socket } = useSocket();
const { socket } = useSocket()
const {
terminals,
@ -22,24 +21,24 @@ export default function Terminals() {
activeTerminalId,
setActiveTerminalId,
creatingTerminal,
} = useTerminal();
} = useTerminal()
const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
// Effect to set the active terminal when a new one is created
useEffect(() => {
if (terminals.length > 0 && !activeTerminalId) {
setActiveTerminalId(terminals[terminals.length - 1].id);
setActiveTerminalId(terminals[terminals.length - 1].id)
}
}, [terminals, activeTerminalId, setActiveTerminalId]);
}, [terminals, activeTerminalId, setActiveTerminalId])
const handleCreateTerminal = () => {
if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals.");
return;
toast.error("You reached the maximum # of terminals.")
return
}
createNewTerminal();
};
createNewTerminal()
}
return (
<>
@ -85,7 +84,7 @@ export default function Terminals() {
? { ...term, terminal: t }
: term
)
);
)
}}
visible={activeTerminalId === term.id}
/>
@ -98,5 +97,5 @@ export default function Terminals() {
</div>
)}
</>
);
}
)
}

View File

@ -1,13 +1,13 @@
"use client"
import { Terminal } from "@xterm/xterm"
import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm"
import "./xterm.css"
import { ElementRef, useEffect, useRef, useState } from "react"
import { Socket } from "socket.io-client"
import { Loader2 } from "lucide-react"
import { debounce } from "@/lib/utils"
import { Loader2 } from "lucide-react"
import { ElementRef, useEffect, useRef } from "react"
import { Socket } from "socket.io-client"
export default function EditorTerminal({
socket,

View File

@ -35,7 +35,7 @@
* Default styles for xterm.js
*/
.xterm {
.xterm {
cursor: text;
position: relative;
user-select: none;
@ -80,7 +80,7 @@
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: transparent;
color: #FFF;
color: #fff;
display: none;
position: absolute;
white-space: nowrap;
@ -154,12 +154,12 @@
}
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent;
color: transparent;
}
.xterm .xterm-accessibility-tree {
user-select: text;
white-space: pre;
user-select: text;
white-space: pre;
}
.xterm .live-region {
@ -176,33 +176,55 @@ white-space: pre;
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-underline-1 {
text-decoration: underline;
}
.xterm-underline-2 {
text-decoration: double underline;
}
.xterm-underline-3 {
text-decoration: wavy underline;
}
.xterm-underline-4 {
text-decoration: dotted underline;
}
.xterm-underline-5 {
text-decoration: dashed underline;
}
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-overline.xterm-underline-1 {
text-decoration: overline underline;
}
.xterm-overline.xterm-underline-2 {
text-decoration: overline double underline;
}
.xterm-overline.xterm-underline-3 {
text-decoration: overline wavy underline;
}
.xterm-overline.xterm-underline-4 {
text-decoration: overline dotted underline;
}
.xterm-overline.xterm-underline-5 {
text-decoration: overline dashed underline;
}
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
.xterm-screen
.xterm-decoration-container
.xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
@ -216,4 +238,4 @@ z-index: 7;
.xterm-decoration-top {
z-index: 2;
position: relative;
}
}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React from "react"
const LoadingDots: React.FC = () => {
return (
@ -9,24 +9,35 @@ const LoadingDots: React.FC = () => {
<style jsx>{`
.loading-dots {
display: inline-block;
font-size: 24px;
font-size: 24px;
}
.dot {
opacity: 0;
animation: showHideDot 1.5s ease-in-out infinite;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.5s; }
.dot:nth-child(3) { animation-delay: 1s; }
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.5s;
}
.dot:nth-child(3) {
animation-delay: 1s;
}
@keyframes showHideDot {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`}</style>
</span>
);
};
export default LoadingDots;
)
}
export default LoadingDots

View File

@ -1,10 +1,10 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from "react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const AlertDialog = AlertDialogPrimitive.Root
@ -128,14 +128,14 @@ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
}

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@ -73,4 +73,4 @@ const CardFooter = React.forwardRef<
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }

View File

@ -1,12 +1,12 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import * as React from "react"
import { cn } from "@/lib/utils"
@ -187,18 +187,18 @@ ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
ContextMenuTrigger,
}

View File

@ -1,18 +1,18 @@
"use client";
"use client"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as React from "react"
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root;
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger;
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal;
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close;
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogContentNoClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@ -70,9 +70,9 @@ const DialogContentNoClose = React.forwardRef<
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
))
DialogContentNoClose.displayName =
DialogPrimitive.Content.displayName + "NoClose";
DialogPrimitive.Content.displayName + "NoClose"
const DialogHeader = ({
className,
@ -85,8 +85,8 @@ const DialogHeader = ({
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
@ -99,8 +99,8 @@ const DialogFooter = ({
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@ -114,8 +114,8 @@ const DialogTitle = React.forwardRef<
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@ -126,19 +126,19 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogContentNoClose,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -1,12 +1,12 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import * as React from "react"
import { cn } from "@/lib/utils"
@ -188,18 +188,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuTrigger,
}

View File

@ -1,6 +1,6 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as React from "react"
import {
Controller,
ControllerProps,
@ -10,8 +10,8 @@ import {
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
const Form = FormProvider
@ -165,12 +165,12 @@ const FormMessage = React.forwardRef<
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
FormItem,
FormLabel,
FormMessage,
useFormField,
}

View File

@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
import { cn } from "@/lib/utils"
@ -30,4 +30,4 @@ const PopoverContent = React.forwardRef<
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }

View File

@ -42,4 +42,4 @@ const ResizableHandle = ({
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }

View File

@ -1,6 +1,5 @@
"use client"
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
@ -8,6 +7,7 @@ import {
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import * as React from "react"
import { cn } from "@/lib/utils"
@ -152,13 +152,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@ -1,8 +1,8 @@
"use client";
"use client"
import { Loader2, X } from "lucide-react";
import { Button } from "./button";
import { MouseEvent, MouseEventHandler, useEffect } from "react";
import { Loader2, X } from "lucide-react"
import { MouseEventHandler } from "react"
import { Button } from "./button"
export default function Tab({
children,
@ -13,13 +13,13 @@ export default function Tab({
onClose,
closing = false,
}: {
children: React.ReactNode;
creating?: boolean;
saved?: boolean;
selected?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>;
onClose?: () => void;
closing?: boolean;
children: React.ReactNode
creating?: boolean
saved?: boolean
selected?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>
onClose?: () => void
closing?: boolean
}) {
return (
<Button
@ -37,9 +37,9 @@ export default function Tab({
onClick={
onClose && !closing
? (e) => {
e.stopPropagation();
e.preventDefault();
onClose();
e.stopPropagation()
e.preventDefault()
onClose()
}
: undefined
}
@ -57,5 +57,5 @@ export default function Tab({
)}
</div>
</Button>
);
)
}

View File

@ -110,11 +110,11 @@ TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
TableCell,
TableCaption,
}

View File

@ -1,23 +1,22 @@
"use client";
"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { User } from "@/lib/types";
import { useClerk } from "@clerk/nextjs";
import { LogOut, Pencil, Sparkles } from "lucide-react";
import { useRouter } from "next/navigation";
} from "@/components/ui/dropdown-menu"
import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs"
import { LogOut, Sparkles } from "lucide-react"
import { useRouter } from "next/navigation"
export default function UserButton({ userData }: { userData: User }) {
if (!userData) return null;
if (!userData) return null
const { signOut } = useClerk();
const router = useRouter();
const { signOut } = useClerk()
const router = useRouter()
return (
<DropdownMenu>
@ -68,5 +67,5 @@ export default function UserButton({ userData }: { userData: User }) {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
)
}

View File

@ -1,34 +1,44 @@
"use client"
import React, { createContext, useContext, useState, useRef } from 'react';
import { ImperativePanelHandle } from "react-resizable-panels";
import React, { createContext, useContext, useRef, useState } from "react"
import { ImperativePanelHandle } from "react-resizable-panels"
interface PreviewContextType {
isPreviewCollapsed: boolean;
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
previewURL: string;
setPreviewURL: React.Dispatch<React.SetStateAction<string>>;
previewPanelRef: React.RefObject<ImperativePanelHandle>;
isPreviewCollapsed: boolean
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>
previewURL: string
setPreviewURL: React.Dispatch<React.SetStateAction<string>>
previewPanelRef: React.RefObject<ImperativePanelHandle>
}
const PreviewContext = createContext<PreviewContextType | undefined>(undefined);
const PreviewContext = createContext<PreviewContextType | undefined>(undefined)
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
const [previewURL, setPreviewURL] = useState<string>("");
const previewPanelRef = useRef<ImperativePanelHandle>(null);
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [previewURL, setPreviewURL] = useState<string>("")
const previewPanelRef = useRef<ImperativePanelHandle>(null)
return (
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}>
<PreviewContext.Provider
value={{
isPreviewCollapsed,
setIsPreviewCollapsed,
previewURL,
setPreviewURL,
previewPanelRef,
}}
>
{children}
</PreviewContext.Provider>
);
};
)
}
export const usePreview = () => {
const context = useContext(PreviewContext);
const context = useContext(PreviewContext)
if (context === undefined) {
throw new Error('usePreview must be used within a PreviewProvider');
throw new Error("usePreview must be used within a PreviewProvider")
}
return context;
};
return context
}

View File

@ -1,63 +1,65 @@
"use client";
"use client"
import React, { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
import React, { createContext, useContext, useEffect, useState } from "react"
import { io, Socket } from "socket.io-client"
interface SocketContextType {
socket: Socket | null;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
socket: Socket | null
setUserAndSandboxId: (userId: string, sandboxId: string) => void
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
const SocketContext = createContext<SocketContextType | undefined>(undefined)
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [socket, setSocket] = useState<Socket | null>(null)
const [userId, setUserId] = useState<string | null>(null)
const [sandboxId, setSandboxId] = useState<string | null>(null)
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
console.log("Initializing socket connection...")
const newSocket = io(
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`
)
console.log("Socket instance:", newSocket)
setSocket(newSocket)
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on("connect", () => {
console.log("Socket connected:", newSocket.id)
})
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
newSocket.on("disconnect", () => {
console.log("Socket disconnected")
})
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
console.log("Disconnecting socket...")
newSocket.disconnect()
}
}
}, [userId, sandboxId]);
}, [userId, sandboxId])
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
setUserId(newUserId)
setSandboxId(newSandboxId)
}
const value = {
socket,
setUserAndSandboxId,
};
}
return (
<SocketContext.Provider value={ value }>
{children}
</SocketContext.Provider>
);
};
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
)
}
export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
const context = useContext(SocketContext)
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
throw new Error("useSocket must be used within a SocketProvider")
}
return context;
};
return context
}

View File

@ -1,33 +1,44 @@
"use client";
"use client"
import React, { createContext, useContext, useState } from 'react';
import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
import { useSocket } from '@/context/SocketContext';
import { useSocket } from "@/context/SocketContext"
import {
closeTerminal as closeTerminalHelper,
createTerminal as createTerminalHelper,
} from "@/lib/terminal"
import { Terminal } from "@xterm/xterm"
import React, { createContext, useContext, useState } from "react"
interface TerminalContextType {
terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string;
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
creatingTerminal: boolean;
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: (command?: string) => Promise<void>;
closeTerminal: (id: string) => void;
deploy: (callback: () => void) => void;
terminals: { id: string; terminal: Terminal | null }[]
setTerminals: React.Dispatch<
React.SetStateAction<{ id: string; terminal: Terminal | null }[]>
>
activeTerminalId: string
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
creatingTerminal: boolean
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
createNewTerminal: (command?: string) => Promise<void>
closeTerminal: (id: string) => void
deploy: (callback: () => void) => void
}
const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
const TerminalContext = createContext<TerminalContextType | undefined>(
undefined
)
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { socket } = useSocket();
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { socket } = useSocket()
const [terminals, setTerminals] = useState<
{ id: string; terminal: Terminal | null }[]
>([])
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return;
setCreatingTerminal(true);
if (!socket) return
setCreatingTerminal(true)
try {
createTerminalHelper({
setTerminals,
@ -35,36 +46,36 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal,
command,
socket,
});
})
} catch (error) {
console.error("Error creating terminal:", error);
console.error("Error creating terminal:", error)
} finally {
setCreatingTerminal(false);
setCreatingTerminal(false)
}
};
}
const closeTerminal = (id: string) => {
if (!socket) return;
const terminalToClose = terminals.find(term => term.id === id);
if (!socket) return
const terminalToClose = terminals.find((term) => term.id === id)
if (terminalToClose) {
closeTerminalHelper({
term: terminalToClose,
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal: () => {},
setClosingTerminal: () => {},
socket,
activeTerminalId,
});
})
}
};
}
const deploy = (callback: () => void) => {
if (!socket) console.error("Couldn't deploy: No socket");
if (!socket) console.error("Couldn't deploy: No socket")
console.log("Deploying...")
socket?.emit("deploy", () => {
callback();
});
callback()
})
}
const value = {
@ -76,20 +87,20 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal,
createNewTerminal,
closeTerminal,
deploy
};
deploy,
}
return (
<TerminalContext.Provider value={value}>
{children}
</TerminalContext.Provider>
);
};
)
}
export const useTerminal = (): TerminalContextType => {
const context = useContext(TerminalContext);
const context = useContext(TerminalContext)
if (!context) {
throw new Error('useTerminal must be used within a TerminalProvider');
throw new Error("useTerminal must be used within a TerminalProvider")
}
return context;
};
return context
}

View File

@ -1,8 +1,8 @@
// Helper functions for terminal instances
import { createId } from "@paralleldrive/cuid2";
import { Terminal } from "@xterm/xterm";
import { Socket } from "socket.io-client";
import { createId } from "@paralleldrive/cuid2"
import { Terminal } from "@xterm/xterm"
import { Socket } from "socket.io-client"
export const createTerminal = ({
setTerminals,
@ -11,30 +11,33 @@ export const createTerminal = ({
command,
socket,
}: {
setTerminals: React.Dispatch<React.SetStateAction<{
id: string;
terminal: Terminal | null;
}[]>>;
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
command?: string;
socket: Socket;
setTerminals: React.Dispatch<
React.SetStateAction<
{
id: string
terminal: Terminal | null
}[]
>
>
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
command?: string
socket: Socket
}) => {
setCreatingTerminal(true);
const id = createId();
console.log("creating terminal, id:", id);
setCreatingTerminal(true)
const id = createId()
console.log("creating terminal, id:", id)
setTerminals((prev) => [...prev, { id, terminal: null }]);
setActiveTerminalId(id);
setTerminals((prev) => [...prev, { id, terminal: null }])
setActiveTerminalId(id)
setTimeout(() => {
socket.emit("createTerminal", id, () => {
setCreatingTerminal(false);
if (command) socket.emit("terminalData", id, command + "\n");
});
}, 1000);
};
setCreatingTerminal(false)
if (command) socket.emit("terminalData", id, command + "\n")
})
}, 1000)
}
export const closeTerminal = ({
term,
@ -44,32 +47,36 @@ export const closeTerminal = ({
setClosingTerminal,
socket,
activeTerminalId,
} : {
term: {
id: string;
terminal: Terminal | null
}: {
term: {
id: string
terminal: Terminal | null
}
terminals: {
id: string;
terminal: Terminal | null
terminals: {
id: string
terminal: Terminal | null
}[]
setTerminals: React.Dispatch<React.SetStateAction<{
id: string;
terminal: Terminal | null
}[]>>
setTerminals: React.Dispatch<
React.SetStateAction<
{
id: string
terminal: Terminal | null
}[]
>
>
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
socket: Socket
activeTerminalId: string
}) => {
const numTerminals = terminals.length;
const index = terminals.findIndex((t) => t.id === term.id);
if (index === -1) return;
const numTerminals = terminals.length
const index = terminals.findIndex((t) => t.id === term.id)
if (index === -1) return
setClosingTerminal(term.id);
setClosingTerminal(term.id)
socket.emit("closeTerminal", term.id, () => {
setClosingTerminal("");
setClosingTerminal("")
const nextId =
activeTerminalId === term.id
@ -78,17 +85,17 @@ export const closeTerminal = ({
: index < numTerminals - 1
? terminals[index + 1].id
: terminals[index - 1].id
: activeTerminalId;
: activeTerminalId
setTerminals((prev) => prev.filter((t) => t.id !== term.id));
setTerminals((prev) => prev.filter((t) => t.id !== term.id))
if (!nextId) {
setActiveTerminalId("");
setActiveTerminalId("")
} else {
const nextTerminal = terminals.find((t) => t.id === nextId);
const nextTerminal = terminals.find((t) => t.id === nextId)
if (nextTerminal) {
setActiveTerminalId(nextTerminal.id);
setActiveTerminalId(nextTerminal.id)
}
}
});
};
})
}

View File

@ -1,65 +1,65 @@
// DB Types
export type User = {
id: string;
name: string;
email: string;
generations: number;
sandbox: Sandbox[];
usersToSandboxes: UsersToSandboxes[];
};
id: string
name: string
email: string
generations: number
sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[]
}
export type Sandbox = {
id: string;
name: string;
type: string;
visibility: "public" | "private";
createdAt: Date;
userId: string;
usersToSandboxes: UsersToSandboxes[];
};
id: string
name: string
type: string
visibility: "public" | "private"
createdAt: Date
userId: string
usersToSandboxes: UsersToSandboxes[]
}
export type UsersToSandboxes = {
userId: string;
sandboxId: string;
sharedOn: Date;
};
userId: string
sandboxId: string
sharedOn: Date
}
export type R2Files = {
objects: R2FileData[];
truncated: boolean;
delimitedPrefixes: any[];
};
objects: R2FileData[]
truncated: boolean
delimitedPrefixes: any[]
}
export type R2FileData = {
storageClass: string;
uploaded: string;
checksums: any;
httpEtag: string;
etag: string;
size: number;
version: string;
key: string;
};
storageClass: string
uploaded: string
checksums: any
httpEtag: string
etag: string
size: number
version: string
key: string
}
export type TFolder = {
id: string;
type: "folder";
name: string;
children: (TFile | TFolder)[];
};
id: string
type: "folder"
name: string
children: (TFile | TFolder)[]
}
export type TFile = {
id: string;
type: "file";
name: string;
};
id: string
type: "file"
name: string
}
export type TTab = TFile & {
saved: boolean;
};
saved: boolean
}
export type TFileData = {
id: string;
data: string;
};
id: string
data: string
}

View File

@ -1,8 +1,8 @@
import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner"
import { twMerge } from "tailwind-merge"
import { Sandbox, TFile, TFolder } from "./types"
import fileExtToLang from "./file-extension-to-language.json"
import { Sandbox, TFile, TFolder } from "./types"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))