Merge branch 'main' of https://github.com/Code-Victor/sandbox into feat/light-theme
This commit is contained in:
commit
9546d4ad7b
@ -1,4 +1,3 @@
|
|||||||
# frontend/**
|
|
||||||
backend/ai/**
|
backend/ai/**
|
||||||
backend/database/**
|
backend/database/**
|
||||||
backend/storage/**
|
backend/storage/**
|
@ -5,3 +5,6 @@ compatibility_flags = ["nodejs_compat"]
|
|||||||
|
|
||||||
[ai]
|
[ai]
|
||||||
binding = "AI"
|
binding = "AI"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
ANTHROPIC_API_KEY = ""
|
||||||
|
@ -207,7 +207,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Error creating container ${data.sandboxId}:`, e)
|
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)
|
await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error setting timeout:", e)
|
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)
|
callback(fileContent)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error getting file:", e)
|
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)
|
callback(files)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error getting folder:", e)
|
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)
|
await fileManager.saveFile(fileId, body)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error saving file:", e)
|
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)
|
callback(newFiles)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error moving file:", e)
|
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 })
|
callback({ success })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error creating file:", e)
|
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()
|
callback()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error creating folder:", e)
|
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)
|
await fileManager.renameFile(fileId, newName)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error renaming file:", e)
|
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)
|
callback(newFiles)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error deleting file:", e)
|
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)
|
callback(newFiles)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error deleting folder:", e)
|
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 {
|
try {
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
await terminalManager.createTerminal(id, (responseString: string) => {
|
await terminalManager.createTerminal(id, (responseString: string) => {
|
||||||
io.emit("terminalResponse", { id, data: responseString })
|
socket.emit("terminalResponse", { id, data: responseString })
|
||||||
const port = extractPortNumber(responseString)
|
const port = extractPortNumber(responseString)
|
||||||
if (port) {
|
if (port) {
|
||||||
io.emit(
|
socket.emit(
|
||||||
"previewURL",
|
"previewURL",
|
||||||
"https://" + containers[data.sandboxId].getHost(port)
|
"https://" + containers[data.sandboxId].getHost(port)
|
||||||
)
|
)
|
||||||
@ -429,7 +429,7 @@ io.on("connection", async (socket) => {
|
|||||||
callback()
|
callback()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Error creating terminal ${id}:`, e)
|
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)
|
terminalManager.resizeTerminal(dimensions)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error resizing terminal:", e)
|
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)
|
await terminalManager.sendTerminalData(id, data)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error writing to terminal:", e)
|
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()
|
callback()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error closing terminal:", e)
|
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)
|
callback(result)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error generating code:", e)
|
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) {
|
} catch (e: any) {
|
||||||
console.log("Error disconnecting:", e)
|
console.log("Error disconnecting:", e)
|
||||||
io.emit("error", `Error: disconnecting. ${e.message ?? e}`)
|
socket.emit("error", `Error: disconnecting. ${e.message ?? e}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error connecting:", e)
|
console.error("Error connecting:", e)
|
||||||
io.emit("error", `Error: connection. ${e.message ?? e}`)
|
socket.emit("error", `Error: connection. ${e.message ?? e}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": false
|
"singleQuote": false,
|
||||||
|
"insertFinalNewline": true
|
||||||
}
|
}
|
@ -1,12 +1,11 @@
|
|||||||
import Navbar from "@/components/editor/navbar"
|
|
||||||
import { Room } from "@/components/editor/live/room"
|
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 { Sandbox, User, UsersToSandboxes } from "@/lib/types"
|
||||||
import { currentUser } from "@clerk/nextjs"
|
import { currentUser } from "@clerk/nextjs"
|
||||||
import { notFound, redirect } from "next/navigation"
|
|
||||||
import Loading from "@/components/editor/loading"
|
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import fs from "fs"
|
import { notFound, redirect } from "next/navigation"
|
||||||
import { TerminalProvider } from "@/context/TerminalContext"
|
|
||||||
|
|
||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
|
|
||||||
@ -92,12 +91,13 @@ export default async function CodePage({ params }: { params: { id: string } }) {
|
|||||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||||
<Room id={sandboxId}>
|
<Room id={sandboxId}>
|
||||||
<TerminalProvider>
|
<TerminalProvider>
|
||||||
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
|
<Navbar
|
||||||
<div className="w-screen flex grow">
|
|
||||||
<CodeEditor
|
|
||||||
userData={userData}
|
userData={userData}
|
||||||
sandboxData={sandboxData}
|
sandboxData={sandboxData}
|
||||||
|
shared={shared}
|
||||||
/>
|
/>
|
||||||
|
<div className="w-screen flex grow">
|
||||||
|
<CodeEditor userData={userData} sandboxData={sandboxData} />
|
||||||
</div>
|
</div>
|
||||||
</TerminalProvider>
|
</TerminalProvider>
|
||||||
</Room>
|
</Room>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { UserButton, currentUser } from "@clerk/nextjs"
|
|
||||||
import { redirect } from "next/navigation"
|
|
||||||
import Dashboard from "@/components/dashboard"
|
import Dashboard from "@/components/dashboard"
|
||||||
import Navbar from "@/components/dashboard/navbar"
|
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() {
|
export default async function DashboardPage() {
|
||||||
const user = await currentUser()
|
const user = await currentUser()
|
||||||
|
@ -15,7 +15,7 @@ export const metadata: Metadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
@ -29,9 +29,7 @@ export default function RootLayout({
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<PreviewProvider>
|
<PreviewProvider>{children}</PreviewProvider>
|
||||||
{children}
|
|
||||||
</PreviewProvider>
|
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<Toaster position="bottom-left" richColors />
|
<Toaster position="bottom-left" richColors />
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { currentUser } from "@clerk/nextjs";
|
import Landing from "@/components/landing"
|
||||||
import { redirect } from "next/navigation";
|
import { currentUser } from "@clerk/nextjs"
|
||||||
import Landing from "@/components/landing";
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const user = await currentUser();
|
const user = await currentUser()
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
redirect("/dashboard");
|
redirect("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Landing />;
|
return <Landing />
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,9 @@
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog"
|
} 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({
|
export default function AboutModal({
|
||||||
open,
|
open,
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import CustomButton from "@/components/ui/customButton"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import CustomButton from "@/components/ui/customButton"
|
||||||
Code2,
|
|
||||||
FolderDot,
|
|
||||||
HelpCircle,
|
|
||||||
Plus,
|
|
||||||
Settings,
|
|
||||||
Users,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { Sandbox } from "@/lib/types"
|
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 DashboardProjects from "./projects"
|
||||||
import DashboardSharedWithMe from "./shared"
|
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"
|
type TScreen = "projects" | "shared" | "settings" | "search"
|
||||||
|
|
||||||
@ -49,7 +41,8 @@ export default function Dashboard({
|
|||||||
const q = searchParams.get("q")
|
const q = searchParams.get("q")
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => { // update the dashboard to show a new project
|
useEffect(() => {
|
||||||
|
// update the dashboard to show a new project
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import Logo from "@/assets/logo.svg"
|
||||||
|
import { User } from "@/lib/types"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import Logo from "@/assets/logo.svg"
|
|
||||||
import DashboardNavbarSearch from "./search"
|
|
||||||
import UserButton from "../../ui/userButton"
|
import UserButton from "../../ui/userButton"
|
||||||
import { User } from "@/lib/types"
|
import DashboardNavbarSearch from "./search"
|
||||||
|
|
||||||
export default function DashboardNavbar({ userData }: { userData: User }) {
|
export default function DashboardNavbar({ userData }: { userData: User }) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Input } from "../../ui/input";
|
import { Search } from "lucide-react"
|
||||||
import { Search } from "lucide-react";
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react";
|
import { Input } from "../../ui/input"
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
export default function DashboardNavbarSearch() {
|
export default function DashboardNavbarSearch() {
|
||||||
// const [search, setSearch] = useState("");
|
// const [search, setSearch] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const delayDebounceFn = setTimeout(() => {
|
// const delayDebounceFn = setTimeout(() => {
|
||||||
@ -29,14 +28,14 @@ export default function DashboardNavbarSearch() {
|
|||||||
// onChange={(e) => setSearch(e.target.value)}
|
// onChange={(e) => setSearch(e.target.value)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value === "") {
|
if (e.target.value === "") {
|
||||||
router.push(`/dashboard`);
|
router.push(`/dashboard`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
router.push(`/dashboard?q=${e.target.value}`);
|
router.push(`/dashboard?q=${e.target.value}`)
|
||||||
}}
|
}}
|
||||||
placeholder="Search projects..."
|
placeholder="Search projects..."
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Sandbox } from "@/lib/types";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react";
|
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
export default function ProjectCardDropdown({
|
export default function ProjectCardDropdown({
|
||||||
sandbox,
|
sandbox,
|
||||||
onVisibilityChange,
|
onVisibilityChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
sandbox: Sandbox;
|
sandbox: Sandbox
|
||||||
onVisibilityChange: (sandbox: Sandbox) => void;
|
onVisibilityChange: (sandbox: Sandbox) => void
|
||||||
onDelete: (sandbox: Sandbox) => void;
|
onDelete: (sandbox: Sandbox) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
e.stopPropagation();
|
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"
|
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">
|
<DropdownMenuContent className="w-40">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onVisibilityChange(sandbox);
|
onVisibilityChange(sandbox)
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
@ -52,8 +52,8 @@ export default function ProjectCardDropdown({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
onDelete(sandbox);
|
onDelete(sandbox)
|
||||||
}}
|
}}
|
||||||
className="!text-destructive cursor-pointer"
|
className="!text-destructive cursor-pointer"
|
||||||
>
|
>
|
||||||
@ -62,5 +62,5 @@ export default function ProjectCardDropdown({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
"use client"
|
"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 { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import { Clock, Globe, Lock } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import ProjectCardDropdown from "./dropdown"
|
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({
|
export default function ProjectCard({
|
||||||
children,
|
children,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client"
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
import { Canvas, useFrame, useThree } from "@react-three/fiber"
|
||||||
import React, { useMemo, useRef } from "react";
|
import React, { useMemo, useRef } from "react"
|
||||||
import * as THREE from "three";
|
import * as THREE from "three"
|
||||||
|
|
||||||
export const CanvasRevealEffect = ({
|
export const CanvasRevealEffect = ({
|
||||||
animationSpeed = 0.4,
|
animationSpeed = 0.4,
|
||||||
@ -12,12 +12,12 @@ export const CanvasRevealEffect = ({
|
|||||||
dotSize,
|
dotSize,
|
||||||
showGradient = true,
|
showGradient = true,
|
||||||
}: {
|
}: {
|
||||||
animationSpeed?: number;
|
animationSpeed?: number
|
||||||
opacities?: number[];
|
opacities?: number[]
|
||||||
colors?: number[][];
|
colors?: number[][]
|
||||||
containerClassName?: string;
|
containerClassName?: string
|
||||||
dotSize?: number;
|
dotSize?: number
|
||||||
showGradient?: boolean;
|
showGradient?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("h-full relative bg-white w-full", containerClassName)}>
|
<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 className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
interface DotMatrixProps {
|
interface DotMatrixProps {
|
||||||
colors?: number[][];
|
colors?: number[][]
|
||||||
opacities?: number[];
|
opacities?: number[]
|
||||||
totalSize?: number;
|
totalSize?: number
|
||||||
dotSize?: number;
|
dotSize?: number
|
||||||
shader?: string;
|
shader?: string
|
||||||
center?: ("x" | "y")[];
|
center?: ("x" | "y")[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const DotMatrix: React.FC<DotMatrixProps> = ({
|
const DotMatrix: React.FC<DotMatrixProps> = ({
|
||||||
@ -69,7 +69,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
colors[0],
|
colors[0],
|
||||||
colors[0],
|
colors[0],
|
||||||
colors[0],
|
colors[0],
|
||||||
];
|
]
|
||||||
if (colors.length === 2) {
|
if (colors.length === 2) {
|
||||||
colorsArray = [
|
colorsArray = [
|
||||||
colors[0],
|
colors[0],
|
||||||
@ -78,7 +78,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
colors[1],
|
colors[1],
|
||||||
colors[1],
|
colors[1],
|
||||||
colors[1],
|
colors[1],
|
||||||
];
|
]
|
||||||
} else if (colors.length === 3) {
|
} else if (colors.length === 3) {
|
||||||
colorsArray = [
|
colorsArray = [
|
||||||
colors[0],
|
colors[0],
|
||||||
@ -87,7 +87,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
colors[1],
|
colors[1],
|
||||||
colors[2],
|
colors[2],
|
||||||
colors[2],
|
colors[2],
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -111,8 +111,8 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
value: dotSize,
|
value: dotSize,
|
||||||
type: "uniform1f",
|
type: "uniform1f",
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}, [colors, opacities, totalSize, dotSize]);
|
}, [colors, opacities, totalSize, dotSize])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shader
|
<Shader
|
||||||
@ -168,87 +168,87 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
|
|||||||
uniforms={uniforms}
|
uniforms={uniforms}
|
||||||
maxFps={60}
|
maxFps={60}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
type Uniforms = {
|
type Uniforms = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
value: number[] | number[][] | number;
|
value: number[] | number[][] | number
|
||||||
type: string;
|
type: string
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
const ShaderMaterial = ({
|
const ShaderMaterial = ({
|
||||||
source,
|
source,
|
||||||
uniforms,
|
uniforms,
|
||||||
maxFps = 60,
|
maxFps = 60,
|
||||||
}: {
|
}: {
|
||||||
source: string;
|
source: string
|
||||||
hovered?: boolean;
|
hovered?: boolean
|
||||||
maxFps?: number;
|
maxFps?: number
|
||||||
uniforms: Uniforms;
|
uniforms: Uniforms
|
||||||
}) => {
|
}) => {
|
||||||
const { size } = useThree();
|
const { size } = useThree()
|
||||||
const ref = useRef<THREE.Mesh>();
|
const ref = useRef<THREE.Mesh>()
|
||||||
let lastFrameTime = 0;
|
let lastFrameTime = 0
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
useFrame(({ clock }) => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return
|
||||||
const timestamp = clock.getElapsedTime();
|
const timestamp = clock.getElapsedTime()
|
||||||
if (timestamp - lastFrameTime < 1 / maxFps) {
|
if (timestamp - lastFrameTime < 1 / maxFps) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
lastFrameTime = timestamp;
|
lastFrameTime = timestamp
|
||||||
|
|
||||||
const material: any = ref.current.material;
|
const material: any = ref.current.material
|
||||||
const timeLocation = material.uniforms.u_time;
|
const timeLocation = material.uniforms.u_time
|
||||||
timeLocation.value = timestamp;
|
timeLocation.value = timestamp
|
||||||
});
|
})
|
||||||
|
|
||||||
const getUniforms = () => {
|
const getUniforms = () => {
|
||||||
const preparedUniforms: any = {};
|
const preparedUniforms: any = {}
|
||||||
|
|
||||||
for (const uniformName in uniforms) {
|
for (const uniformName in uniforms) {
|
||||||
const uniform: any = uniforms[uniformName];
|
const uniform: any = uniforms[uniformName]
|
||||||
|
|
||||||
switch (uniform.type) {
|
switch (uniform.type) {
|
||||||
case "uniform1f":
|
case "uniform1f":
|
||||||
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
|
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" }
|
||||||
break;
|
break
|
||||||
case "uniform3f":
|
case "uniform3f":
|
||||||
preparedUniforms[uniformName] = {
|
preparedUniforms[uniformName] = {
|
||||||
value: new THREE.Vector3().fromArray(uniform.value),
|
value: new THREE.Vector3().fromArray(uniform.value),
|
||||||
type: "3f",
|
type: "3f",
|
||||||
};
|
}
|
||||||
break;
|
break
|
||||||
case "uniform1fv":
|
case "uniform1fv":
|
||||||
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
|
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }
|
||||||
break;
|
break
|
||||||
case "uniform3fv":
|
case "uniform3fv":
|
||||||
preparedUniforms[uniformName] = {
|
preparedUniforms[uniformName] = {
|
||||||
value: uniform.value.map((v: number[]) =>
|
value: uniform.value.map((v: number[]) =>
|
||||||
new THREE.Vector3().fromArray(v)
|
new THREE.Vector3().fromArray(v)
|
||||||
),
|
),
|
||||||
type: "3fv",
|
type: "3fv",
|
||||||
};
|
}
|
||||||
break;
|
break
|
||||||
case "uniform2f":
|
case "uniform2f":
|
||||||
preparedUniforms[uniformName] = {
|
preparedUniforms[uniformName] = {
|
||||||
value: new THREE.Vector2().fromArray(uniform.value),
|
value: new THREE.Vector2().fromArray(uniform.value),
|
||||||
type: "2f",
|
type: "2f",
|
||||||
};
|
}
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
console.error(`Invalid uniform type for '${uniformName}'.`);
|
console.error(`Invalid uniform type for '${uniformName}'.`)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preparedUniforms["u_time"] = { value: 0, type: "1f" };
|
preparedUniforms["u_time"] = { value: 0, type: "1f" }
|
||||||
preparedUniforms["u_resolution"] = {
|
preparedUniforms["u_resolution"] = {
|
||||||
value: new THREE.Vector2(size.width * 2, size.height * 2),
|
value: new THREE.Vector2(size.width * 2, size.height * 2),
|
||||||
}; // Initialize u_resolution
|
} // Initialize u_resolution
|
||||||
return preparedUniforms;
|
return preparedUniforms
|
||||||
};
|
}
|
||||||
|
|
||||||
// Shader material
|
// Shader material
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
@ -272,33 +272,33 @@ const ShaderMaterial = ({
|
|||||||
blending: THREE.CustomBlending,
|
blending: THREE.CustomBlending,
|
||||||
blendSrc: THREE.SrcAlphaFactor,
|
blendSrc: THREE.SrcAlphaFactor,
|
||||||
blendDst: THREE.OneFactor,
|
blendDst: THREE.OneFactor,
|
||||||
});
|
})
|
||||||
|
|
||||||
return materialObject;
|
return materialObject
|
||||||
}, [size.width, size.height, source]);
|
}, [size.width, size.height, source])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh ref={ref as any}>
|
<mesh ref={ref as any}>
|
||||||
<planeGeometry args={[2, 2]} />
|
<planeGeometry args={[2, 2]} />
|
||||||
<primitive object={material} attach="material" />
|
<primitive object={material} attach="material" />
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
|
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
|
||||||
return (
|
return (
|
||||||
<Canvas className="absolute inset-0 h-full w-full">
|
<Canvas className="absolute inset-0 h-full w-full">
|
||||||
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
|
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
interface ShaderProps {
|
interface ShaderProps {
|
||||||
source: string;
|
source: string
|
||||||
uniforms: {
|
uniforms: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
value: number[] | number[][] | number;
|
value: number[] | number[][] | number
|
||||||
type: string;
|
type: string
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
maxFps?: number;
|
maxFps?: number
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Sandbox } from "@/lib/types";
|
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||||
import ProjectCard from "./projectCard";
|
import { Sandbox } from "@/lib/types"
|
||||||
import Image from "next/image";
|
import Link from "next/link"
|
||||||
import ProjectCardDropdown from "./projectCard/dropdown";
|
import { useEffect, useState } from "react"
|
||||||
import { Clock, Globe, Lock } from "lucide-react";
|
import { toast } from "sonner"
|
||||||
import Link from "next/link";
|
import ProjectCard from "./projectCard"
|
||||||
import { Card } from "../ui/card";
|
import { CanvasRevealEffect } from "./projectCard/revealEffect"
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { CanvasRevealEffect } from "./projectCard/revealEffect";
|
|
||||||
|
|
||||||
const colors: { [key: string]: number[][] } = {
|
const colors: { [key: string]: number[][] } = {
|
||||||
react: [
|
react: [
|
||||||
@ -21,38 +17,37 @@ const colors: { [key: string]: number[][] } = {
|
|||||||
[86, 184, 72],
|
[86, 184, 72],
|
||||||
[59, 112, 52],
|
[59, 112, 52],
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function DashboardProjects({
|
export default function DashboardProjects({
|
||||||
sandboxes,
|
sandboxes,
|
||||||
q,
|
q,
|
||||||
}: {
|
}: {
|
||||||
sandboxes: Sandbox[];
|
sandboxes: Sandbox[]
|
||||||
q: string | null;
|
q: string | null
|
||||||
}) {
|
}) {
|
||||||
const [deletingId, setDeletingId] = useState<string>("");
|
const [deletingId, setDeletingId] = useState<string>("")
|
||||||
|
|
||||||
const onDelete = async (sandbox: Sandbox) => {
|
const onDelete = async (sandbox: Sandbox) => {
|
||||||
setDeletingId(sandbox.id);
|
setDeletingId(sandbox.id)
|
||||||
toast(`Project ${sandbox.name} deleted.`);
|
toast(`Project ${sandbox.name} deleted.`)
|
||||||
await deleteSandbox(sandbox.id);
|
await deleteSandbox(sandbox.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deletingId) {
|
if (deletingId) {
|
||||||
setDeletingId("");
|
setDeletingId("")
|
||||||
}
|
}
|
||||||
}, [sandboxes]);
|
}, [sandboxes])
|
||||||
|
|
||||||
const onVisibilityChange = async (sandbox: Sandbox) => {
|
const onVisibilityChange = async (sandbox: Sandbox) => {
|
||||||
const newVisibility =
|
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
|
||||||
sandbox.visibility === "public" ? "private" : "public";
|
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
||||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`);
|
|
||||||
await updateSandbox({
|
await updateSandbox({
|
||||||
id: sandbox.id,
|
id: sandbox.id,
|
||||||
visibility: newVisibility,
|
visibility: newVisibility,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grow p-4 flex flex-col">
|
<div className="grow p-4 flex flex-col">
|
||||||
@ -65,7 +60,7 @@ export default function DashboardProjects({
|
|||||||
{sandboxes.map((sandbox) => {
|
{sandboxes.map((sandbox) => {
|
||||||
if (q && q.length > 0) {
|
if (q && q.length > 0) {
|
||||||
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
|
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
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" />
|
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
||||||
</ProjectCard>
|
</ProjectCard>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -103,5 +98,5 @@ export default function DashboardProjects({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,27 @@
|
|||||||
import { Sandbox } from "@/lib/types";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table"
|
||||||
import Image from "next/image";
|
import { ChevronRight } from "lucide-react"
|
||||||
import Button from "../ui/customButton";
|
import Image from "next/image"
|
||||||
import { ChevronRight } from "lucide-react";
|
import Link from "next/link"
|
||||||
import Avatar from "../ui/avatar";
|
import Avatar from "../ui/avatar"
|
||||||
import Link from "next/link";
|
import Button from "../ui/customButton"
|
||||||
|
|
||||||
export default function DashboardSharedWithMe({
|
export default function DashboardSharedWithMe({
|
||||||
shared,
|
shared,
|
||||||
}: {
|
}: {
|
||||||
shared: {
|
shared: {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
type: "react" | "node";
|
type: "react" | "node"
|
||||||
author: string;
|
author: string
|
||||||
sharedOn: Date;
|
sharedOn: Date
|
||||||
}[];
|
}[]
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grow p-4 flex flex-col">
|
<div className="grow p-4 flex flex-col">
|
||||||
@ -86,5 +84,5 @@ export default function DashboardSharedWithMe({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,51 @@
|
|||||||
import React from 'react';
|
import { Send, StopCircle } from "lucide-react"
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from "../../ui/button"
|
||||||
import { Send, StopCircle } from 'lucide-react';
|
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
input: string;
|
input: string
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void
|
||||||
isGenerating: boolean;
|
isGenerating: boolean
|
||||||
handleSend: () => void;
|
handleSend: () => void
|
||||||
handleStopGeneration: () => void;
|
handleStopGeneration: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) {
|
export default function ChatInput({
|
||||||
|
input,
|
||||||
|
setInput,
|
||||||
|
isGenerating,
|
||||||
|
handleSend,
|
||||||
|
handleStopGeneration,
|
||||||
|
}: ChatInputProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 min-w-0">
|
<div className="flex space-x-2 min-w-0">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
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"
|
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
disabled={isGenerating}
|
disabled={isGenerating}
|
||||||
/>
|
/>
|
||||||
{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" />
|
<StopCircle className="w-4 h-4" />
|
||||||
</Button>
|
</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" />
|
<Send className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import React, { useState } from 'react';
|
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
|
||||||
import { Button } from '../../ui/button';
|
import React, { useState } from "react"
|
||||||
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
|
import ReactMarkdown from "react-markdown"
|
||||||
import ReactMarkdown from 'react-markdown';
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
import remarkGfm from "remark-gfm"
|
||||||
import remarkGfm from 'remark-gfm';
|
import { Button } from "../../ui/button"
|
||||||
import { copyToClipboard, stringifyContent } from './lib/chatUtils';
|
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
message: {
|
message: {
|
||||||
role: 'user' | 'assistant';
|
role: "user" | "assistant"
|
||||||
content: string;
|
content: string
|
||||||
context?: string;
|
context?: string
|
||||||
};
|
}
|
||||||
setContext: (context: string | null) => void;
|
setContext: (context: string | null) => void
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) {
|
export default function ChatMessage({
|
||||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
|
message,
|
||||||
const [copiedText, setCopiedText] = useState<string | null>(null);
|
setContext,
|
||||||
|
setIsContextExpanded,
|
||||||
|
}: MessageProps) {
|
||||||
|
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null)
|
||||||
|
const [copiedText, setCopiedText] = useState<string | null>(null)
|
||||||
|
|
||||||
const renderCopyButton = (text: any) => (
|
const renderCopyButton = (text: any) => (
|
||||||
<Button
|
<Button
|
||||||
@ -34,17 +40,17 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<Copy className="w-4 h-4" />
|
<Copy className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
|
|
||||||
const askAboutCode = (code: any) => {
|
const askAboutCode = (code: any) => {
|
||||||
const contextString = stringifyContent(code);
|
const contextString = stringifyContent(code)
|
||||||
setContext(`Regarding this code:\n${contextString}`);
|
setContext(`Regarding this code:\n${contextString}`)
|
||||||
setIsContextExpanded(false);
|
setIsContextExpanded(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const renderMarkdownElement = (props: any) => {
|
const renderMarkdownElement = (props: any) => {
|
||||||
const { node, children } = props;
|
const { node, children } = props
|
||||||
const content = stringifyContent(children);
|
const content = stringifyContent(children)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
@ -59,22 +65,30 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<CornerUpLeft className="w-4 h-4" />
|
<CornerUpLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{React.createElement(node.tagName, {
|
{React.createElement(
|
||||||
|
node.tagName,
|
||||||
|
{
|
||||||
...props,
|
...props,
|
||||||
className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors`
|
className: `${
|
||||||
}, children)}
|
props.className || ""
|
||||||
|
} hover:bg-transparent rounded p-1 transition-colors`,
|
||||||
|
},
|
||||||
|
children
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-left relative">
|
<div className="text-left relative">
|
||||||
<div className={`relative p-2 rounded-lg ${
|
<div
|
||||||
message.role === 'user'
|
className={`relative p-2 rounded-lg ${
|
||||||
? 'bg-[#262626] text-white'
|
message.role === "user"
|
||||||
: 'bg-transparent text-white'
|
? "bg-[#262626] text-white"
|
||||||
} max-w-full`}>
|
: "bg-transparent text-white"
|
||||||
{message.role === 'user' && (
|
} max-w-full`}
|
||||||
|
>
|
||||||
|
{message.role === "user" && (
|
||||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
||||||
{renderCopyButton(message.content)}
|
{renderCopyButton(message.content)}
|
||||||
<Button
|
<Button
|
||||||
@ -91,11 +105,11 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<div className="mb-2 bg-input rounded-lg">
|
<div className="mb-2 bg-input rounded-lg">
|
||||||
<div
|
<div
|
||||||
className="flex justify-between items-center cursor-pointer"
|
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">
|
<span className="text-sm text-gray-300">Context</span>
|
||||||
Context
|
|
||||||
</span>
|
|
||||||
{expandedMessageIndex === 0 ? (
|
{expandedMessageIndex === 0 ? (
|
||||||
<ChevronUp size={16} />
|
<ChevronUp size={16} />
|
||||||
) : (
|
) : (
|
||||||
@ -105,41 +119,46 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
{expandedMessageIndex === 0 && (
|
{expandedMessageIndex === 0 && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute top-0 right-0 flex p-1">
|
<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>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const code = message.context.replace(/^Regarding this code:\n/, '');
|
const code = message.context.replace(
|
||||||
const match = /language-(\w+)/.exec(code);
|
/^Regarding this code:\n/,
|
||||||
const language = match ? match[1] : 'typescript';
|
""
|
||||||
|
)
|
||||||
|
const match = /language-(\w+)/.exec(code)
|
||||||
|
const language = match ? match[1] : "typescript"
|
||||||
return (
|
return (
|
||||||
<div className="pt-6">
|
<div className="pt-6">
|
||||||
<textarea
|
<textarea
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updatedContext = `Regarding this code:\n${e.target.value}`;
|
const updatedContext = `Regarding this code:\n${e.target.value}`
|
||||||
setContext(updatedContext);
|
setContext(updatedContext)
|
||||||
}}
|
}}
|
||||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
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={{
|
style={{
|
||||||
resize: 'vertical',
|
resize: "vertical",
|
||||||
minHeight: '100px',
|
minHeight: "100px",
|
||||||
maxHeight: '400px',
|
maxHeight: "400px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.role === 'assistant' ? (
|
{message.role === "assistant" ? (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
code({ node, className, children, ...props }) {
|
code({ node, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || "")
|
||||||
return match ? (
|
return match ? (
|
||||||
<div className="relative border border-input rounded-md my-4">
|
<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">
|
<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"
|
PreTag="div"
|
||||||
customStyle={{
|
customStyle={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: '0.5rem',
|
padding: "0.5rem",
|
||||||
fontSize: '0.875rem',
|
fontSize: "0.875rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stringifyContent(children)}
|
{stringifyContent(children)}
|
||||||
@ -175,7 +194,7 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
p: renderMarkdownElement,
|
p: renderMarkdownElement,
|
||||||
h1: renderMarkdownElement,
|
h1: renderMarkdownElement,
|
||||||
@ -184,18 +203,24 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
|||||||
h4: renderMarkdownElement,
|
h4: renderMarkdownElement,
|
||||||
h5: renderMarkdownElement,
|
h5: renderMarkdownElement,
|
||||||
h6: renderMarkdownElement,
|
h6: renderMarkdownElement,
|
||||||
ul: (props) => <ul className="list-disc pl-6 mb-4 space-y-2">{props.children}</ul>,
|
ul: (props) => (
|
||||||
ol: (props) => <ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>,
|
<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}
|
{message.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
<div className="whitespace-pre-wrap group">
|
<div className="whitespace-pre-wrap group">{message.content}</div>
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import React from 'react';
|
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
||||||
import { ChevronUp, ChevronDown, X } from 'lucide-react';
|
|
||||||
|
|
||||||
interface ContextDisplayProps {
|
interface ContextDisplayProps {
|
||||||
context: string | null;
|
context: string | null
|
||||||
isContextExpanded: boolean;
|
isContextExpanded: boolean
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
setContext: (context: string | null) => void;
|
setContext: (context: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContextDisplay({ context, isContextExpanded, setIsContextExpanded, setContext }: ContextDisplayProps) {
|
export default function ContextDisplay({
|
||||||
if (!context) return null;
|
context,
|
||||||
|
isContextExpanded,
|
||||||
|
setIsContextExpanded,
|
||||||
|
setContext,
|
||||||
|
}: ContextDisplayProps) {
|
||||||
|
if (!context) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 bg-input p-2 rounded-lg">
|
<div className="mb-2 bg-input p-2 rounded-lg">
|
||||||
@ -18,15 +22,21 @@ export default function ContextDisplay({ context, isContextExpanded, setIsContex
|
|||||||
className="flex-grow cursor-pointer"
|
className="flex-grow cursor-pointer"
|
||||||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm text-gray-300">Context</span>
|
||||||
Context
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isContextExpanded ? (
|
{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
|
<X
|
||||||
size={16}
|
size={16}
|
||||||
@ -37,12 +47,14 @@ export default function ContextDisplay({ context, isContextExpanded, setIsContex
|
|||||||
</div>
|
</div>
|
||||||
{isContextExpanded && (
|
{isContextExpanded && (
|
||||||
<textarea
|
<textarea
|
||||||
value={context.replace(/^Regarding this code:\n/, '')}
|
value={context.replace(/^Regarding this code:\n/, "")}
|
||||||
onChange={(e) => setContext(`Regarding this code:\n${e.target.value}`)}
|
onChange={(e) =>
|
||||||
|
setContext(`Regarding this code:\n${e.target.value}`)
|
||||||
|
}
|
||||||
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,48 +1,72 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import { X } from "lucide-react"
|
||||||
import LoadingDots from '../../ui/LoadingDots';
|
import { useEffect, useRef, useState } from "react"
|
||||||
import ChatMessage from './ChatMessage';
|
import LoadingDots from "../../ui/LoadingDots"
|
||||||
import ChatInput from './ChatInput';
|
import ChatInput from "./ChatInput"
|
||||||
import ContextDisplay from './ContextDisplay';
|
import ChatMessage from "./ChatMessage"
|
||||||
import { handleSend, handleStopGeneration } from './lib/chatUtils';
|
import ContextDisplay from "./ContextDisplay"
|
||||||
|
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
role: 'user' | 'assistant';
|
role: "user" | "assistant"
|
||||||
content: string;
|
content: string
|
||||||
context?: string;
|
context?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AIChat({ activeFileContent, activeFileName }: { activeFileContent: string, activeFileName: string }) {
|
export default function AIChat({
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
activeFileContent,
|
||||||
const [input, setInput] = useState('');
|
activeFileName,
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
onClose,
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
}: {
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
activeFileContent: string
|
||||||
const [context, setContext] = useState<string | null>(null);
|
activeFileName: string
|
||||||
const [isContextExpanded, setIsContextExpanded] = useState(false);
|
onClose: () => void
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
}) {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom()
|
||||||
}, [messages]);
|
}, [messages])
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (chatContainerRef.current) {
|
if (chatContainerRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chatContainerRef.current?.scrollTo({
|
chatContainerRef.current?.scrollTo({
|
||||||
top: chatContainerRef.current.scrollHeight,
|
top: chatContainerRef.current.scrollHeight,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
})
|
||||||
}, 100);
|
}, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen w-full">
|
<div className="flex flex-col h-screen w-full">
|
||||||
<div className="flex justify-between items-center p-2 border-b">
|
<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">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>
|
||||||
|
<div
|
||||||
|
ref={chatContainerRef}
|
||||||
|
className="flex-grow overflow-y-auto p-4 space-y-4"
|
||||||
|
>
|
||||||
{messages.map((message, messageIndex) => (
|
{messages.map((message, messageIndex) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={messageIndex}
|
key={messageIndex}
|
||||||
@ -64,10 +88,23 @@ export default function AIChat({ activeFileContent, activeFileName }: { activeFi
|
|||||||
input={input}
|
input={input}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
isGenerating={isGenerating}
|
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)}
|
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,58 +1,68 @@
|
|||||||
import React from 'react';
|
import React from "react"
|
||||||
|
|
||||||
export const stringifyContent = (content: any, seen = new WeakSet()): string => {
|
export const stringifyContent = (
|
||||||
if (typeof content === 'string') {
|
content: any,
|
||||||
return content;
|
seen = new WeakSet()
|
||||||
|
): string => {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
if (content === null) {
|
if (content === null) {
|
||||||
return 'null';
|
return "null"
|
||||||
}
|
}
|
||||||
if (content === undefined) {
|
if (content === undefined) {
|
||||||
return 'undefined';
|
return "undefined"
|
||||||
}
|
}
|
||||||
if (typeof content === 'number' || typeof content === 'boolean') {
|
if (typeof content === "number" || typeof content === "boolean") {
|
||||||
return content.toString();
|
return content.toString()
|
||||||
}
|
}
|
||||||
if (typeof content === 'function') {
|
if (typeof content === "function") {
|
||||||
return content.toString();
|
return content.toString()
|
||||||
}
|
}
|
||||||
if (typeof content === 'symbol') {
|
if (typeof content === "symbol") {
|
||||||
return content.toString();
|
return content.toString()
|
||||||
}
|
}
|
||||||
if (typeof content === 'bigint') {
|
if (typeof content === "bigint") {
|
||||||
return content.toString() + 'n';
|
return content.toString() + "n"
|
||||||
}
|
}
|
||||||
if (React.isValidElement(content)) {
|
if (React.isValidElement(content)) {
|
||||||
return React.Children.toArray((content as React.ReactElement).props.children)
|
return React.Children.toArray(
|
||||||
.map(child => stringifyContent(child, seen))
|
(content as React.ReactElement).props.children
|
||||||
.join('');
|
)
|
||||||
|
.map((child) => stringifyContent(child, seen))
|
||||||
|
.join("")
|
||||||
}
|
}
|
||||||
if (Array.isArray(content)) {
|
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)) {
|
if (seen.has(content)) {
|
||||||
return '[Circular]';
|
return "[Circular]"
|
||||||
}
|
}
|
||||||
seen.add(content);
|
seen.add(content)
|
||||||
try {
|
try {
|
||||||
const pairs = Object.entries(content).map(
|
const pairs = Object.entries(content).map(
|
||||||
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
|
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
|
||||||
);
|
)
|
||||||
return '{' + pairs.join(', ') + '}';
|
return "{" + pairs.join(", ") + "}"
|
||||||
} catch (error) {
|
} 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(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setCopiedText(text);
|
setCopiedText(text)
|
||||||
setTimeout(() => setCopiedText(null), 2000);
|
setTimeout(() => setCopiedText(null), 2000)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const handleSend = async (
|
export const handleSend = async (
|
||||||
input: string,
|
input: string,
|
||||||
@ -66,32 +76,34 @@ export const handleSend = async (
|
|||||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||||
activeFileContent: string
|
activeFileContent: string
|
||||||
) => {
|
) => {
|
||||||
if (input.trim() === '' && !context) return;
|
if (input.trim() === "" && !context) return
|
||||||
|
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
role: 'user' as const,
|
role: "user" as const,
|
||||||
content: input,
|
content: input,
|
||||||
context: context || undefined
|
context: context || undefined,
|
||||||
};
|
}
|
||||||
const updatedMessages = [...messages, newMessage];
|
const updatedMessages = [...messages, newMessage]
|
||||||
setMessages(updatedMessages);
|
setMessages(updatedMessages)
|
||||||
setInput('');
|
setInput("")
|
||||||
setIsContextExpanded(false);
|
setIsContextExpanded(false)
|
||||||
setIsGenerating(true);
|
setIsGenerating(true)
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController();
|
abortControllerRef.current = new AbortController()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const anthropicMessages = updatedMessages.map(msg => ({
|
const anthropicMessages = updatedMessages.map((msg) => ({
|
||||||
role: msg.role === 'user' ? 'human' : 'assistant',
|
role: msg.role === "user" ? "human" : "assistant",
|
||||||
content: msg.content
|
content: msg.content,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: anthropicMessages,
|
messages: anthropicMessages,
|
||||||
@ -99,64 +111,70 @@ export const handleSend = async (
|
|||||||
activeFileContent: activeFileContent,
|
activeFileContent: activeFileContent,
|
||||||
}),
|
}),
|
||||||
signal: abortControllerRef.current.signal,
|
signal: abortControllerRef.current.signal,
|
||||||
});
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (!response.ok) {
|
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 reader = response.body?.getReader()
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder()
|
||||||
const assistantMessage = { role: 'assistant' as const, content: '' };
|
const assistantMessage = { role: "assistant" as const, content: "" }
|
||||||
setMessages([...updatedMessages, assistantMessage]);
|
setMessages([...updatedMessages, assistantMessage])
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
|
|
||||||
let buffer = '';
|
let buffer = ""
|
||||||
const updateInterval = 100;
|
const updateInterval = 100
|
||||||
let lastUpdateTime = Date.now();
|
let lastUpdateTime = Date.now()
|
||||||
|
|
||||||
if (reader) {
|
if (reader) {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read()
|
||||||
if (done) break;
|
if (done) break
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now()
|
||||||
if (currentTime - lastUpdateTime > updateInterval) {
|
if (currentTime - lastUpdateTime > updateInterval) {
|
||||||
setMessages(prev => {
|
setMessages((prev) => {
|
||||||
const updatedMessages = [...prev];
|
const updatedMessages = [...prev]
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||||
lastMessage.content = buffer;
|
lastMessage.content = buffer
|
||||||
return updatedMessages;
|
return updatedMessages
|
||||||
});
|
})
|
||||||
lastUpdateTime = currentTime;
|
lastUpdateTime = currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev => {
|
setMessages((prev) => {
|
||||||
const updatedMessages = [...prev];
|
const updatedMessages = [...prev]
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||||
lastMessage.content = buffer;
|
lastMessage.content = buffer
|
||||||
return updatedMessages;
|
return updatedMessages
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === "AbortError") {
|
||||||
console.log('Generation aborted');
|
console.log("Generation aborted")
|
||||||
} else {
|
} else {
|
||||||
console.error('Error fetching AI response:', error);
|
console.error("Error fetching AI response:", error)
|
||||||
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' };
|
const errorMessage = {
|
||||||
setMessages(prev => [...prev, errorMessage]);
|
role: "assistant" as const,
|
||||||
|
content: "Sorry, I encountered an error. Please try again.",
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, errorMessage])
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false)
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => {
|
export const handleStopGeneration = (
|
||||||
|
abortControllerRef: React.MutableRefObject<AbortController | null>
|
||||||
|
) => {
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"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 { 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 { 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"
|
// import monaco from "monaco-editor"
|
||||||
|
|
||||||
export default function GenerateInput({
|
export default function GenerateInput({
|
||||||
|
@ -91,6 +91,7 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Layout state
|
// Layout state
|
||||||
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
|
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
|
||||||
|
const [previousLayout, setPreviousLayout] = useState(false)
|
||||||
|
|
||||||
// AI Chat state
|
// AI Chat state
|
||||||
const [isAIChatOpen, setIsAIChatOpen] = useState(false)
|
const [isAIChatOpen, setIsAIChatOpen] = useState(false)
|
||||||
@ -548,12 +549,18 @@ export default function CodeEditor({
|
|||||||
setIsAIChatOpen((prev) => !prev)
|
setIsAIChatOpen((prev) => !prev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", down)
|
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 () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down)
|
||||||
}
|
}
|
||||||
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen])
|
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -868,8 +875,25 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLayout = () => {
|
const toggleLayout = () => {
|
||||||
|
if (!isAIChatOpen) {
|
||||||
setIsHorizontalLayout((prev) => !prev)
|
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
|
// On disabled access for shared users, show un-interactable loading placeholder + info modal
|
||||||
if (disableAccess.isDisabled)
|
if (disableAccess.isDisabled)
|
||||||
@ -1007,7 +1031,9 @@ export default function CodeEditor({
|
|||||||
deletingFolderId={deletingFolderId}
|
deletingFolderId={deletingFolderId}
|
||||||
/>
|
/>
|
||||||
{/* Outer ResizablePanelGroup for main layout */}
|
{/* Outer ResizablePanelGroup for main layout */}
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup
|
||||||
|
direction={isHorizontalLayout ? "horizontal" : "vertical"}
|
||||||
|
>
|
||||||
{/* Left side: Editor and Preview/Terminal */}
|
{/* Left side: Editor and Preview/Terminal */}
|
||||||
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
|
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
@ -1136,6 +1162,7 @@ export default function CodeEditor({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="mr-2 border"
|
className="mr-2 border"
|
||||||
|
disabled={isAIChatOpen}
|
||||||
>
|
>
|
||||||
{isHorizontalLayout ? (
|
{isHorizontalLayout ? (
|
||||||
<ArrowRightToLine className="w-4 h-4" />
|
<ArrowRightToLine className="w-4 h-4" />
|
||||||
@ -1190,6 +1217,7 @@ export default function CodeEditor({
|
|||||||
tabs.find((tab) => tab.id === activeFileId)?.name ||
|
tabs.find((tab) => tab.id === activeFileId)?.name ||
|
||||||
"No file selected"
|
"No file selected"
|
||||||
}
|
}
|
||||||
|
onClose={toggleAIChat}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useOthers } from "@/liveblocks.config";
|
import { useOthers } from "@/liveblocks.config"
|
||||||
|
|
||||||
const classNames = {
|
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",
|
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:
|
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",
|
"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",
|
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() {
|
export function Avatars() {
|
||||||
const users = useOthers();
|
const users = useOthers()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -30,12 +30,12 @@ export function Avatars() {
|
|||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((letter) => letter[0].toUpperCase())}
|
.map((letter) => letter[0].toUpperCase())}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{users.length > 0 ? (
|
{users.length > 0 ? (
|
||||||
<div className="h-full w-[1px] bg-border mx-2" />
|
<div className="h-full w-[1px] bg-border mx-2" />
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from "react"
|
import { colors } from "@/lib/colors"
|
||||||
import {
|
import {
|
||||||
AwarenessList,
|
AwarenessList,
|
||||||
TypedLiveblocksProvider,
|
TypedLiveblocksProvider,
|
||||||
UserAwareness,
|
UserAwareness,
|
||||||
useSelf,
|
|
||||||
} from "@/liveblocks.config"
|
} from "@/liveblocks.config"
|
||||||
import { colors } from "@/lib/colors"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
|
||||||
export function Cursors({
|
export function Cursors({
|
||||||
yProvider,
|
yProvider,
|
||||||
|
@ -1,43 +1,35 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
} from "@/components/ui/dialog"
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
import {
|
import { Loader2 } from "lucide-react"
|
||||||
ChevronRight,
|
import { useRouter } from "next/navigation"
|
||||||
FileStack,
|
import { useEffect } from "react"
|
||||||
Globe,
|
|
||||||
Loader2,
|
|
||||||
TextCursor,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function DisableAccessModal({
|
export default function DisableAccessModal({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
message,
|
message,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void
|
||||||
message: string;
|
message: string
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
router.push("/dashboard");
|
router.push("/dashboard")
|
||||||
}, 5000);
|
}, 5000)
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@ -54,5 +46,5 @@ export default function DisableAccessModal({
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { RoomProvider } from "@/liveblocks.config";
|
import { RoomProvider } from "@/liveblocks.config"
|
||||||
import { ClientSideSuspense } from "@liveblocks/react";
|
|
||||||
|
|
||||||
export function Room({
|
export function Room({
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RoomProvider
|
<RoomProvider
|
||||||
@ -21,5 +20,5 @@ export function Room({
|
|||||||
{children}
|
{children}
|
||||||
{/* </ClientSideSuspense> */}
|
{/* </ClientSideSuspense> */}
|
||||||
</RoomProvider>
|
</RoomProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Image from "next/image"
|
|
||||||
import Logo from "@/assets/logo.svg"
|
import Logo from "@/assets/logo.svg"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
|
||||||
import { Loader2, X } from "lucide-react"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -11,6 +8,9 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} 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"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
export default function Loading({
|
export default function Loading({
|
||||||
|
@ -1,34 +1,38 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react";
|
import { Button } from "@/components/ui/button"
|
||||||
import { Button } from "@/components/ui/button";
|
import {
|
||||||
import { useTerminal } from "@/context/TerminalContext";
|
Popover,
|
||||||
import { Play, Pause, Globe, Globe2 } from "lucide-react";
|
PopoverContent,
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
PopoverTrigger,
|
||||||
import { Sandbox, User } from "@/lib/types";
|
} 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({
|
export default function DeployButtonModal({
|
||||||
userData,
|
userData,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
userData: User;
|
userData: User
|
||||||
data: Sandbox;
|
data: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const { deploy } = useTerminal();
|
const { deploy } = useTerminal()
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
|
|
||||||
const handleDeploy = () => {
|
const handleDeploy = () => {
|
||||||
if (isDeploying) {
|
if (isDeploying) {
|
||||||
console.log("Stopping deployment...");
|
console.log("Stopping deployment...")
|
||||||
setIsDeploying(false);
|
setIsDeploying(false)
|
||||||
} else {
|
} else {
|
||||||
console.log("Starting deployment...");
|
console.log("Starting deployment...")
|
||||||
setIsDeploying(true);
|
setIsDeploying(true)
|
||||||
deploy(() => {
|
deploy(() => {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false)
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -39,7 +43,10 @@ export default function DeployButtonModal({
|
|||||||
Deploy
|
Deploy
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</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>
|
<h3 className="font-semibold text-gray-300 mb-2">Domains</h3>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<DeploymentOption
|
<DeploymentOption
|
||||||
@ -49,16 +56,30 @@ export default function DeployButtonModal({
|
|||||||
user={userData.name}
|
user={userData.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" onClick={handleDeploy}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
|
||||||
|
onClick={handleDeploy}
|
||||||
|
>
|
||||||
{isDeploying ? "Deploying..." : "Update"}
|
{isDeploying ? "Deploying..." : "Update"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</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 (
|
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 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">
|
<div className="flex items-start gap-2 relative">
|
||||||
@ -72,7 +93,9 @@ function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.React
|
|||||||
{domain}
|
{domain}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,57 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
} from "@/components/ui/dialog"
|
||||||
} from "@/components/ui/dialog";
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { z } from "zod";
|
import { useForm } from "react-hook-form"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { z } from "zod"
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select"
|
||||||
import { Loader2 } from "lucide-react";
|
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||||
import { useState } from "react";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { Sandbox } from "@/lib/types";
|
import { Loader2 } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button";
|
import { useRouter } from "next/navigation"
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions";
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation";
|
import { toast } from "sonner"
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1).max(16),
|
name: z.string().min(1).max(16),
|
||||||
visibility: z.enum(["public", "private"]),
|
visibility: z.enum(["public", "private"]),
|
||||||
});
|
})
|
||||||
|
|
||||||
export default function EditSandboxModal({
|
export default function EditSandboxModal({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void
|
||||||
data: Sandbox;
|
data: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false)
|
||||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
const [loadingDelete, setLoadingDelete] = useState(false)
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -62,22 +59,22 @@ export default function EditSandboxModal({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
await updateSandbox({ id: data.id, ...values });
|
await updateSandbox({ id: data.id, ...values })
|
||||||
|
|
||||||
toast.success("Sandbox updated successfully");
|
toast.success("Sandbox updated successfully")
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
setLoadingDelete(true);
|
setLoadingDelete(true)
|
||||||
await deleteSandbox(data.id);
|
await deleteSandbox(data.id)
|
||||||
|
|
||||||
router.push("/dashboard");
|
router.push("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -153,5 +150,5 @@ export default function EditSandboxModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,73 +1,78 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import { Button } from "@/components/ui/button"
|
||||||
import { Play, StopCircle } from "lucide-react";
|
import { usePreview } from "@/context/PreviewContext"
|
||||||
import { Button } from "@/components/ui/button";
|
import { useTerminal } from "@/context/TerminalContext"
|
||||||
import { useTerminal } from "@/context/TerminalContext";
|
import { Sandbox } from "@/lib/types"
|
||||||
import { usePreview } from "@/context/PreviewContext";
|
import { Play, StopCircle } from "lucide-react"
|
||||||
import { toast } from "sonner";
|
import { useEffect, useRef } from "react"
|
||||||
import { Sandbox } from "@/lib/types";
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default function RunButtonModal({
|
export default function RunButtonModal({
|
||||||
isRunning,
|
isRunning,
|
||||||
setIsRunning,
|
setIsRunning,
|
||||||
sandboxData,
|
sandboxData,
|
||||||
}: {
|
}: {
|
||||||
isRunning: boolean;
|
isRunning: boolean
|
||||||
setIsRunning: (running: boolean) => void;
|
setIsRunning: (running: boolean) => void
|
||||||
sandboxData: Sandbox;
|
sandboxData: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const { createNewTerminal, closeTerminal, terminals } = useTerminal();
|
const { createNewTerminal, closeTerminal, terminals } = useTerminal()
|
||||||
const { setIsPreviewCollapsed, previewPanelRef } = usePreview();
|
const { setIsPreviewCollapsed, previewPanelRef } = usePreview()
|
||||||
// Ref to keep track of the last created terminal's ID
|
// 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
|
// Effect to update the lastCreatedTerminalRef when a new terminal is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminals.length > 0 && !isRunning) {
|
if (terminals.length > 0 && !isRunning) {
|
||||||
const latestTerminal = terminals[terminals.length - 1];
|
const latestTerminal = terminals[terminals.length - 1]
|
||||||
if (latestTerminal && latestTerminal.id !== lastCreatedTerminalRef.current) {
|
if (
|
||||||
lastCreatedTerminalRef.current = latestTerminal.id;
|
latestTerminal &&
|
||||||
|
latestTerminal.id !== lastCreatedTerminalRef.current
|
||||||
|
) {
|
||||||
|
lastCreatedTerminalRef.current = latestTerminal.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [terminals, isRunning]);
|
}, [terminals, isRunning])
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
if (isRunning && lastCreatedTerminalRef.current)
|
if (isRunning && lastCreatedTerminalRef.current) {
|
||||||
{
|
await closeTerminal(lastCreatedTerminalRef.current)
|
||||||
await closeTerminal(lastCreatedTerminalRef.current);
|
lastCreatedTerminalRef.current = null
|
||||||
lastCreatedTerminalRef.current = null;
|
setIsPreviewCollapsed(true)
|
||||||
setIsPreviewCollapsed(true);
|
previewPanelRef.current?.collapse()
|
||||||
previewPanelRef.current?.collapse();
|
} else if (!isRunning && terminals.length < 4) {
|
||||||
}
|
const command =
|
||||||
else if (!isRunning && terminals.length < 4)
|
sandboxData.type === "streamlit"
|
||||||
{
|
|
||||||
const command = sandboxData.type === "streamlit"
|
|
||||||
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
|
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
|
||||||
: "yarn install && yarn dev";
|
: "yarn install && yarn dev"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a new terminal with the appropriate command
|
// Create a new terminal with the appropriate command
|
||||||
await createNewTerminal(command);
|
await createNewTerminal(command)
|
||||||
setIsPreviewCollapsed(false);
|
setIsPreviewCollapsed(false)
|
||||||
previewPanelRef.current?.expand();
|
previewPanelRef.current?.expand()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to create new terminal.");
|
toast.error("Failed to create new terminal.")
|
||||||
console.error("Error creating new terminal:", error);
|
console.error("Error creating new terminal:", error)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
} else if (!isRunning) {
|
} else if (!isRunning) {
|
||||||
toast.error("You've reached the maximum number of terminals.");
|
toast.error("You've reached the maximum number of terminals.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRunning(!isRunning);
|
setIsRunning(!isRunning)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" onClick={handleRun}>
|
<Button variant="outline" onClick={handleRun}>
|
||||||
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />}
|
{isRunning ? (
|
||||||
{isRunning ? 'Stop' : 'Run'}
|
<StopCircle className="w-4 h-4 mr-2" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isRunning ? "Stop" : "Run"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -6,10 +6,11 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { z } from "zod"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -18,14 +19,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
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 { 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 { toast } from "sonner"
|
||||||
import SharedUser from "./sharedUser"
|
import SharedUser from "./sharedUser"
|
||||||
import { DialogDescription } from "@radix-ui/react-dialog"
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { Link, RotateCw, UnfoldVertical } from "lucide-react"
|
||||||
import {
|
import {
|
||||||
Link,
|
forwardRef,
|
||||||
RotateCw,
|
useEffect,
|
||||||
TerminalSquare,
|
useImperativeHandle,
|
||||||
UnfoldVertical,
|
useRef,
|
||||||
} from "lucide-react"
|
useState,
|
||||||
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"
|
} from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export default forwardRef(function PreviewWindow({
|
export default forwardRef(function PreviewWindow(
|
||||||
|
{
|
||||||
collapsed,
|
collapsed,
|
||||||
open,
|
open,
|
||||||
src
|
src,
|
||||||
}: {
|
}: {
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
open: () => void
|
open: () => void
|
||||||
@ -20,11 +22,12 @@ export default forwardRef(function PreviewWindow({
|
|||||||
},
|
},
|
||||||
ref: React.Ref<{
|
ref: React.Ref<{
|
||||||
refreshIframe: () => void
|
refreshIframe: () => void
|
||||||
}>) {
|
}>
|
||||||
|
) {
|
||||||
const frameRef = useRef<HTMLIFrameElement>(null)
|
const frameRef = useRef<HTMLIFrameElement>(null)
|
||||||
const [iframeKey, setIframeKey] = useState(0)
|
const [iframeKey, setIframeKey] = useState(0)
|
||||||
const refreshIframe = () => {
|
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])
|
useEffect(refreshIframe, [src])
|
||||||
@ -76,7 +79,8 @@ function PreviewButton({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${disabled ? "pointer-events-none opacity-50" : ""
|
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`}
|
} 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}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
@ -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 {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu"
|
||||||
import { Loader2, Pencil, Trash2 } from "lucide-react";
|
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({
|
export default function SidebarFile({
|
||||||
data,
|
data,
|
||||||
@ -22,36 +22,36 @@ export default function SidebarFile({
|
|||||||
movingId,
|
movingId,
|
||||||
deletingFolderId,
|
deletingFolderId,
|
||||||
}: {
|
}: {
|
||||||
data: TFile;
|
data: TFile
|
||||||
selectFile: (file: TTab) => void;
|
selectFile: (file: TTab) => void
|
||||||
handleRename: (
|
handleRename: (
|
||||||
id: string,
|
id: string,
|
||||||
newName: string,
|
newName: string,
|
||||||
oldName: string,
|
oldName: string,
|
||||||
type: "file" | "folder"
|
type: "file" | "folder"
|
||||||
) => boolean;
|
) => boolean
|
||||||
handleDeleteFile: (file: TFile) => void;
|
handleDeleteFile: (file: TFile) => void
|
||||||
movingId: string;
|
movingId: string
|
||||||
deletingFolderId: string;
|
deletingFolderId: string
|
||||||
}) {
|
}) {
|
||||||
const isMoving = movingId === data.id;
|
const isMoving = movingId === data.id
|
||||||
const isDeleting =
|
const isDeleting =
|
||||||
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
|
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId)
|
||||||
|
|
||||||
const ref = useRef(null); // for draggable
|
const ref = useRef(null) // for draggable
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false)
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`);
|
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`)
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false)
|
||||||
const [pendingDelete, setPendingDelete] = useState(isDeleting);
|
const [pendingDelete, setPendingDelete] = useState(isDeleting)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPendingDelete(isDeleting);
|
setPendingDelete(isDeleting)
|
||||||
}, [isDeleting]);
|
}, [isDeleting])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = ref.current;
|
const el = ref.current
|
||||||
|
|
||||||
if (el)
|
if (el)
|
||||||
return draggable({
|
return draggable({
|
||||||
@ -59,14 +59,14 @@ export default function SidebarFile({
|
|||||||
onDragStart: () => setDragging(true),
|
onDragStart: () => setDragging(true),
|
||||||
onDrop: () => setDragging(false),
|
onDrop: () => setDragging(false),
|
||||||
getInitialData: () => ({ id: data.id }),
|
getInitialData: () => ({ id: data.id }),
|
||||||
});
|
})
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
setTimeout(() => inputRef.current?.focus(), 0);
|
setTimeout(() => inputRef.current?.focus(), 0)
|
||||||
}
|
}
|
||||||
}, [editing, inputRef.current]);
|
}, [editing, inputRef.current])
|
||||||
|
|
||||||
const renameFile = () => {
|
const renameFile = () => {
|
||||||
const renamed = handleRename(
|
const renamed = handleRename(
|
||||||
@ -74,12 +74,12 @@ export default function SidebarFile({
|
|||||||
inputRef.current?.value ?? data.name,
|
inputRef.current?.value ?? data.name,
|
||||||
data.name,
|
data.name,
|
||||||
"file"
|
"file"
|
||||||
);
|
)
|
||||||
if (!renamed && inputRef.current) {
|
if (!renamed && inputRef.current) {
|
||||||
inputRef.current.value = data.name;
|
inputRef.current.value = data.name
|
||||||
|
}
|
||||||
|
setEditing(false)
|
||||||
}
|
}
|
||||||
setEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
@ -88,7 +88,7 @@ export default function SidebarFile({
|
|||||||
disabled={pendingDelete || dragging || isMoving}
|
disabled={pendingDelete || dragging || isMoving}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!editing && !pendingDelete && !isMoving)
|
if (!editing && !pendingDelete && !isMoving)
|
||||||
selectFile({ ...data, saved: true });
|
selectFile({ ...data, saved: true })
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
@ -119,8 +119,8 @@ export default function SidebarFile({
|
|||||||
) : (
|
) : (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
renameFile();
|
renameFile()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -138,8 +138,8 @@ export default function SidebarFile({
|
|||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("rename");
|
console.log("rename")
|
||||||
setEditing(true);
|
setEditing(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="w-4 h-4 mr-2" />
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
@ -148,9 +148,9 @@ export default function SidebarFile({
|
|||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
disabled={pendingDelete}
|
disabled={pendingDelete}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("delete");
|
console.log("delete")
|
||||||
setPendingDelete(true);
|
setPendingDelete(true)
|
||||||
handleDeleteFile(data);
|
handleDeleteFile(data)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
@ -158,5 +158,5 @@ export default function SidebarFile({
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
"use client"
|
"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 {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react"
|
import { TFile, TFolder, TTab } from "@/lib/types"
|
||||||
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
|
||||||
import { cn } from "@/lib/utils"
|
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
|
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
|
||||||
|
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
|
||||||
FilePlus,
|
import { FilePlus, FolderPlus, MessageSquareMore, Sparkles } from "lucide-react"
|
||||||
FolderPlus,
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
Loader2,
|
import { Socket } from "socket.io-client"
|
||||||
MonitorPlay,
|
|
||||||
Search,
|
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react"
|
|
||||||
import SidebarFile from "./file"
|
import SidebarFile from "./file"
|
||||||
import SidebarFolder from "./folder"
|
import SidebarFolder from "./folder"
|
||||||
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
|
||||||
import New from "./new"
|
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 {
|
import {
|
||||||
dropTargetForElements,
|
dropTargetForElements,
|
||||||
monitorForElements,
|
monitorForElements,
|
||||||
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
|
} 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({
|
export default function Sidebar({
|
||||||
sandboxData,
|
sandboxData,
|
||||||
@ -107,8 +99,8 @@ export default function Sidebar({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-56 select-none flex flex-col text-sm items-start justify-between p-2">
|
<div className="h-full w-56 select-none flex flex-col text-sm">
|
||||||
<div className="w-full flex flex-col items-start">
|
<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="flex w-full items-center justify-between h-8 mb-1">
|
||||||
<div className="text-muted-foreground">Explorer</div>
|
<div className="text-muted-foreground">Explorer</div>
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
@ -185,10 +177,37 @@ export default function Sidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-4">
|
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
|
||||||
{/* <Button className="w-full">
|
<Button
|
||||||
<MonitorPlay className="w-4 h-4 mr-2" /> Run
|
variant="ghost"
|
||||||
</Button> */}
|
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { validateName } from "@/lib/utils";
|
import { validateName } from "@/lib/utils"
|
||||||
import Image from "next/image";
|
import Image from "next/image"
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react"
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client"
|
||||||
|
|
||||||
export default function New({
|
export default function New({
|
||||||
socket,
|
socket,
|
||||||
@ -11,18 +11,18 @@ export default function New({
|
|||||||
stopEditing,
|
stopEditing,
|
||||||
addNew,
|
addNew,
|
||||||
}: {
|
}: {
|
||||||
socket: Socket;
|
socket: Socket
|
||||||
type: "file" | "folder";
|
type: "file" | "folder"
|
||||||
stopEditing: () => void;
|
stopEditing: () => void
|
||||||
addNew: (name: string, type: "file" | "folder") => void;
|
addNew: (name: string, type: "file" | "folder") => void
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
function createNew() {
|
function createNew() {
|
||||||
const name = inputRef.current?.value;
|
const name = inputRef.current?.value
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
const valid = validateName(name, "", type);
|
const valid = validateName(name, "", type)
|
||||||
if (valid.status) {
|
if (valid.status) {
|
||||||
if (type === "file") {
|
if (type === "file") {
|
||||||
socket.emit(
|
socket.emit(
|
||||||
@ -30,23 +30,23 @@ export default function New({
|
|||||||
name,
|
name,
|
||||||
({ success }: { success: boolean }) => {
|
({ success }: { success: boolean }) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
addNew(name, type);
|
addNew(name, type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
socket.emit("createFolder", name, () => {
|
socket.emit("createFolder", name, () => {
|
||||||
addNew(name, type);
|
addNew(name, type)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopEditing();
|
stopEditing()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus()
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return (
|
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">
|
<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
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
createNew();
|
createNew()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -74,5 +74,5 @@ export default function New({
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button"
|
||||||
import Tab from "@/components/ui/tab";
|
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 { useSocket } from "@/context/SocketContext"
|
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() {
|
export default function Terminals() {
|
||||||
|
const { socket } = useSocket()
|
||||||
const { socket } = useSocket();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
terminals,
|
terminals,
|
||||||
@ -22,24 +21,24 @@ export default function Terminals() {
|
|||||||
activeTerminalId,
|
activeTerminalId,
|
||||||
setActiveTerminalId,
|
setActiveTerminalId,
|
||||||
creatingTerminal,
|
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
|
// Effect to set the active terminal when a new one is created
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (terminals.length > 0 && !activeTerminalId) {
|
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 = () => {
|
const handleCreateTerminal = () => {
|
||||||
if (terminals.length >= 4) {
|
if (terminals.length >= 4) {
|
||||||
toast.error("You reached the maximum # of terminals.");
|
toast.error("You reached the maximum # of terminals.")
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
|
createNewTerminal()
|
||||||
}
|
}
|
||||||
createNewTerminal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -85,7 +84,7 @@ export default function Terminals() {
|
|||||||
? { ...term, terminal: t }
|
? { ...term, terminal: t }
|
||||||
: term
|
: term
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}}
|
}}
|
||||||
visible={activeTerminalId === term.id}
|
visible={activeTerminalId === term.id}
|
||||||
/>
|
/>
|
||||||
@ -98,5 +97,5 @@ export default function Terminals() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Terminal } from "@xterm/xterm"
|
|
||||||
import { FitAddon } from "@xterm/addon-fit"
|
import { FitAddon } from "@xterm/addon-fit"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
import "./xterm.css"
|
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 { 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({
|
export default function EditorTerminal({
|
||||||
socket,
|
socket,
|
||||||
|
@ -80,7 +80,7 @@
|
|||||||
.xterm .composition-view {
|
.xterm .composition-view {
|
||||||
/* TODO: Composition position got messed up somewhere */
|
/* TODO: Composition position got messed up somewhere */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -176,21 +176,41 @@ white-space: pre;
|
|||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-underline-1 { text-decoration: underline; }
|
.xterm-underline-1 {
|
||||||
.xterm-underline-2 { text-decoration: double underline; }
|
text-decoration: underline;
|
||||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
}
|
||||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
.xterm-underline-2 {
|
||||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
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 {
|
.xterm-overline {
|
||||||
text-decoration: overline;
|
text-decoration: overline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
.xterm-overline.xterm-underline-1 {
|
||||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
text-decoration: overline 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-2 {
|
||||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
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 {
|
.xterm-strikethrough {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
@ -201,7 +221,9 @@ z-index: 6;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
.xterm-screen
|
||||||
|
.xterm-decoration-container
|
||||||
|
.xterm-decoration.xterm-decoration-top-layer {
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React from "react"
|
||||||
|
|
||||||
const LoadingDots: React.FC = () => {
|
const LoadingDots: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -15,18 +15,29 @@ const LoadingDots: React.FC = () => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: showHideDot 1.5s ease-in-out infinite;
|
animation: showHideDot 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.dot:nth-child(1) { animation-delay: 0s; }
|
.dot:nth-child(1) {
|
||||||
.dot:nth-child(2) { animation-delay: 0.5s; }
|
animation-delay: 0s;
|
||||||
.dot:nth-child(3) { animation-delay: 1s; }
|
}
|
||||||
|
.dot:nth-child(2) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
.dot:nth-child(3) {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
@keyframes showHideDot {
|
@keyframes showHideDot {
|
||||||
0% { opacity: 0; }
|
0% {
|
||||||
50% { opacity: 1; }
|
opacity: 0;
|
||||||
100% { opacity: 0; }
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</span>
|
</span>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LoadingDots;
|
|
||||||
|
|
||||||
|
export default LoadingDots
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
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 { buttonVariants } from "@/components/ui/button"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
@ -128,14 +128,14 @@ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
@ -73,4 +73,4 @@ const CardFooter = React.forwardRef<
|
|||||||
))
|
))
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from "@radix-ui/react-icons"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -187,18 +187,18 @@ ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
ContextMenuCheckboxItem,
|
||||||
ContextMenuRadioItem,
|
ContextMenuContent,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuItem,
|
||||||
ContextMenuLabel,
|
ContextMenuLabel,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
ContextMenuRadioItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuShortcut,
|
ContextMenuShortcut,
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
ContextMenuSub,
|
||||||
ContextMenuSubContent,
|
ContextMenuSubContent,
|
||||||
ContextMenuSubTrigger,
|
ContextMenuSubTrigger,
|
||||||
ContextMenuRadioGroup,
|
ContextMenuTrigger,
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import * as React from "react";
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||||
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<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
|||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
))
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
const DialogContentNoClose = React.forwardRef<
|
const DialogContentNoClose = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
@ -70,9 +70,9 @@ const DialogContentNoClose = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
))
|
||||||
DialogContentNoClose.displayName =
|
DialogContentNoClose.displayName =
|
||||||
DialogPrimitive.Content.displayName + "NoClose";
|
DialogPrimitive.Content.displayName + "NoClose"
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
@ -85,8 +85,8 @@ const DialogHeader = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
DialogHeader.displayName = "DialogHeader";
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
@ -99,8 +99,8 @@ const DialogFooter = ({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
DialogFooter.displayName = "DialogFooter";
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
@ -114,8 +114,8 @@ const DialogTitle = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
@ -126,19 +126,19 @@ const DialogDescription = React.forwardRef<
|
|||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentNoClose,
|
DialogContentNoClose,
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
};
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from "@radix-ui/react-icons"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -188,18 +188,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuTrigger,
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
ControllerProps,
|
ControllerProps,
|
||||||
@ -10,8 +10,8 @@ import {
|
|||||||
useFormContext,
|
useFormContext,
|
||||||
} from "react-hook-form"
|
} from "react-hook-form"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Form = FormProvider
|
const Form = FormProvider
|
||||||
|
|
||||||
@ -165,12 +165,12 @@ const FormMessage = React.forwardRef<
|
|||||||
FormMessage.displayName = "FormMessage"
|
FormMessage.displayName = "FormMessage"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
|
||||||
FormField,
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useFormField,
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -30,4 +30,4 @@ const PopoverContent = React.forwardRef<
|
|||||||
))
|
))
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
||||||
|
@ -42,4 +42,4 @@ const ResizableHandle = ({
|
|||||||
</ResizablePrimitive.PanelResizeHandle>
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
import {
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
@ -8,6 +7,7 @@ import {
|
|||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from "@radix-ui/react-icons"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@ -152,13 +152,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { Loader2, X } from "lucide-react";
|
import { Loader2, X } from "lucide-react"
|
||||||
import { Button } from "./button";
|
import { MouseEventHandler } from "react"
|
||||||
import { MouseEvent, MouseEventHandler, useEffect } from "react";
|
import { Button } from "./button"
|
||||||
|
|
||||||
export default function Tab({
|
export default function Tab({
|
||||||
children,
|
children,
|
||||||
@ -13,13 +13,13 @@ export default function Tab({
|
|||||||
onClose,
|
onClose,
|
||||||
closing = false,
|
closing = false,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
creating?: boolean;
|
creating?: boolean
|
||||||
saved?: boolean;
|
saved?: boolean
|
||||||
selected?: boolean;
|
selected?: boolean
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
closing?: boolean;
|
closing?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -37,9 +37,9 @@ export default function Tab({
|
|||||||
onClick={
|
onClick={
|
||||||
onClose && !closing
|
onClose && !closing
|
||||||
? (e) => {
|
? (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
onClose();
|
onClose()
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -57,5 +57,5 @@ export default function Tab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -110,11 +110,11 @@ TableCaption.displayName = "TableCaption"
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { User } from "@/lib/types";
|
import { User } from "@/lib/types"
|
||||||
import { useClerk } from "@clerk/nextjs";
|
import { useClerk } from "@clerk/nextjs"
|
||||||
import { LogOut, Pencil, Sparkles } from "lucide-react";
|
import { LogOut, Sparkles } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
export default function UserButton({ userData }: { userData: User }) {
|
export default function UserButton({ userData }: { userData: User }) {
|
||||||
if (!userData) return null;
|
if (!userData) return null
|
||||||
|
|
||||||
const { signOut } = useClerk();
|
const { signOut } = useClerk()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -68,5 +67,5 @@ export default function UserButton({ userData }: { userData: User }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,44 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useRef } from 'react';
|
import React, { createContext, useContext, useRef, useState } from "react"
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
|
|
||||||
interface PreviewContextType {
|
interface PreviewContextType {
|
||||||
isPreviewCollapsed: boolean;
|
isPreviewCollapsed: boolean
|
||||||
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
previewURL: string;
|
previewURL: string
|
||||||
setPreviewURL: React.Dispatch<React.SetStateAction<string>>;
|
setPreviewURL: React.Dispatch<React.SetStateAction<string>>
|
||||||
previewPanelRef: React.RefObject<ImperativePanelHandle>;
|
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 }) => {
|
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true);
|
children,
|
||||||
const [previewURL, setPreviewURL] = useState<string>("");
|
}) => {
|
||||||
const previewPanelRef = useRef<ImperativePanelHandle>(null);
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||||
|
const [previewURL, setPreviewURL] = useState<string>("")
|
||||||
|
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}>
|
<PreviewContext.Provider
|
||||||
|
value={{
|
||||||
|
isPreviewCollapsed,
|
||||||
|
setIsPreviewCollapsed,
|
||||||
|
previewURL,
|
||||||
|
setPreviewURL,
|
||||||
|
previewPanelRef,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</PreviewContext.Provider>
|
</PreviewContext.Provider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const usePreview = () => {
|
export const usePreview = () => {
|
||||||
const context = useContext(PreviewContext);
|
const context = useContext(PreviewContext)
|
||||||
if (context === undefined) {
|
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;
|
|
||||||
};
|
|
@ -1,63 +1,65 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from "react"
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from "socket.io-client"
|
||||||
|
|
||||||
interface SocketContextType {
|
interface SocketContextType {
|
||||||
socket: Socket | null;
|
socket: Socket | null
|
||||||
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
|
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 }) => {
|
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
children,
|
||||||
const [userId, setUserId] = useState<string | null>(null);
|
}) => {
|
||||||
const [sandboxId, setSandboxId] = useState<string | null>(null);
|
const [socket, setSocket] = useState<Socket | null>(null)
|
||||||
|
const [userId, setUserId] = useState<string | null>(null)
|
||||||
|
const [sandboxId, setSandboxId] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userId && sandboxId) {
|
if (userId && sandboxId) {
|
||||||
console.log("Initializing socket connection...");
|
console.log("Initializing socket connection...")
|
||||||
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
|
const newSocket = io(
|
||||||
console.log("Socket instance:", newSocket);
|
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`
|
||||||
setSocket(newSocket);
|
)
|
||||||
|
console.log("Socket instance:", newSocket)
|
||||||
|
setSocket(newSocket)
|
||||||
|
|
||||||
newSocket.on('connect', () => {
|
newSocket.on("connect", () => {
|
||||||
console.log("Socket connected:", newSocket.id);
|
console.log("Socket connected:", newSocket.id)
|
||||||
});
|
})
|
||||||
|
|
||||||
newSocket.on('disconnect', () => {
|
newSocket.on("disconnect", () => {
|
||||||
console.log("Socket disconnected");
|
console.log("Socket disconnected")
|
||||||
});
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("Disconnecting socket...");
|
console.log("Disconnecting socket...")
|
||||||
newSocket.disconnect();
|
newSocket.disconnect()
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [userId, sandboxId]);
|
}
|
||||||
|
}, [userId, sandboxId])
|
||||||
|
|
||||||
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
|
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
|
||||||
setUserId(newUserId);
|
setUserId(newUserId)
|
||||||
setSandboxId(newSandboxId);
|
setSandboxId(newSandboxId)
|
||||||
};
|
}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
socket,
|
socket,
|
||||||
setUserAndSandboxId,
|
setUserAndSandboxId,
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={ value }>
|
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||||
{children}
|
)
|
||||||
</SocketContext.Provider>
|
}
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSocket = (): SocketContextType => {
|
export const useSocket = (): SocketContextType => {
|
||||||
const context = useContext(SocketContext);
|
const context = useContext(SocketContext)
|
||||||
if (!context) {
|
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;
|
|
||||||
};
|
|
||||||
|
@ -1,33 +1,44 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useState } from 'react';
|
import { useSocket } from "@/context/SocketContext"
|
||||||
import { Terminal } from '@xterm/xterm';
|
import {
|
||||||
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
|
closeTerminal as closeTerminalHelper,
|
||||||
import { useSocket } from '@/context/SocketContext';
|
createTerminal as createTerminalHelper,
|
||||||
|
} from "@/lib/terminal"
|
||||||
|
import { Terminal } from "@xterm/xterm"
|
||||||
|
import React, { createContext, useContext, useState } from "react"
|
||||||
|
|
||||||
interface TerminalContextType {
|
interface TerminalContextType {
|
||||||
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<
|
||||||
activeTerminalId: string;
|
React.SetStateAction<{ id: string; terminal: Terminal | null }[]>
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
>
|
||||||
creatingTerminal: boolean;
|
activeTerminalId: string
|
||||||
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
createNewTerminal: (command?: string) => Promise<void>;
|
creatingTerminal: boolean
|
||||||
closeTerminal: (id: string) => void;
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
deploy: (callback: () => void) => void;
|
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 }) => {
|
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const { socket } = useSocket();
|
children,
|
||||||
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
|
}) => {
|
||||||
const [activeTerminalId, setActiveTerminalId] = useState<string>('');
|
const { socket } = useSocket()
|
||||||
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
|
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> => {
|
const createNewTerminal = async (command?: string): Promise<void> => {
|
||||||
if (!socket) return;
|
if (!socket) return
|
||||||
setCreatingTerminal(true);
|
setCreatingTerminal(true)
|
||||||
try {
|
try {
|
||||||
createTerminalHelper({
|
createTerminalHelper({
|
||||||
setTerminals,
|
setTerminals,
|
||||||
@ -35,17 +46,17 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
setCreatingTerminal,
|
setCreatingTerminal,
|
||||||
command,
|
command,
|
||||||
socket,
|
socket,
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating terminal:", error);
|
console.error("Error creating terminal:", error)
|
||||||
} finally {
|
} finally {
|
||||||
setCreatingTerminal(false);
|
setCreatingTerminal(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const closeTerminal = (id: string) => {
|
const closeTerminal = (id: string) => {
|
||||||
if (!socket) return;
|
if (!socket) return
|
||||||
const terminalToClose = terminals.find(term => term.id === id);
|
const terminalToClose = terminals.find((term) => term.id === id)
|
||||||
if (terminalToClose) {
|
if (terminalToClose) {
|
||||||
closeTerminalHelper({
|
closeTerminalHelper({
|
||||||
term: terminalToClose,
|
term: terminalToClose,
|
||||||
@ -55,16 +66,16 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
setClosingTerminal: () => {},
|
setClosingTerminal: () => {},
|
||||||
socket,
|
socket,
|
||||||
activeTerminalId,
|
activeTerminalId,
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const deploy = (callback: () => void) => {
|
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...")
|
console.log("Deploying...")
|
||||||
socket?.emit("deploy", () => {
|
socket?.emit("deploy", () => {
|
||||||
callback();
|
callback()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
@ -76,20 +87,20 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
setCreatingTerminal,
|
setCreatingTerminal,
|
||||||
createNewTerminal,
|
createNewTerminal,
|
||||||
closeTerminal,
|
closeTerminal,
|
||||||
deploy
|
deploy,
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TerminalContext.Provider value={value}>
|
<TerminalContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</TerminalContext.Provider>
|
</TerminalContext.Provider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useTerminal = (): TerminalContextType => {
|
export const useTerminal = (): TerminalContextType => {
|
||||||
const context = useContext(TerminalContext);
|
const context = useContext(TerminalContext)
|
||||||
if (!context) {
|
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;
|
|
||||||
};
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Helper functions for terminal instances
|
// Helper functions for terminal instances
|
||||||
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm"
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client"
|
||||||
|
|
||||||
export const createTerminal = ({
|
export const createTerminal = ({
|
||||||
setTerminals,
|
setTerminals,
|
||||||
@ -11,30 +11,33 @@ export const createTerminal = ({
|
|||||||
command,
|
command,
|
||||||
socket,
|
socket,
|
||||||
}: {
|
}: {
|
||||||
setTerminals: React.Dispatch<React.SetStateAction<{
|
setTerminals: React.Dispatch<
|
||||||
id: string;
|
React.SetStateAction<
|
||||||
terminal: Terminal | null;
|
{
|
||||||
}[]>>;
|
id: string
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>;
|
terminal: Terminal | null
|
||||||
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
|
}[]
|
||||||
command?: string;
|
>
|
||||||
socket: Socket;
|
>
|
||||||
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
command?: string
|
||||||
|
socket: Socket
|
||||||
}) => {
|
}) => {
|
||||||
setCreatingTerminal(true);
|
setCreatingTerminal(true)
|
||||||
const id = createId();
|
const id = createId()
|
||||||
console.log("creating terminal, id:", id);
|
console.log("creating terminal, id:", id)
|
||||||
|
|
||||||
setTerminals((prev) => [...prev, { id, terminal: null }]);
|
setTerminals((prev) => [...prev, { id, terminal: null }])
|
||||||
setActiveTerminalId(id);
|
setActiveTerminalId(id)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
socket.emit("createTerminal", id, () => {
|
socket.emit("createTerminal", id, () => {
|
||||||
setCreatingTerminal(false);
|
setCreatingTerminal(false)
|
||||||
if (command) socket.emit("terminalData", id, command + "\n");
|
if (command) socket.emit("terminalData", id, command + "\n")
|
||||||
});
|
})
|
||||||
}, 1000);
|
}, 1000)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const closeTerminal = ({
|
export const closeTerminal = ({
|
||||||
term,
|
term,
|
||||||
@ -46,30 +49,34 @@ export const closeTerminal = ({
|
|||||||
activeTerminalId,
|
activeTerminalId,
|
||||||
}: {
|
}: {
|
||||||
term: {
|
term: {
|
||||||
id: string;
|
id: string
|
||||||
terminal: Terminal | null
|
terminal: Terminal | null
|
||||||
}
|
}
|
||||||
terminals: {
|
terminals: {
|
||||||
id: string;
|
id: string
|
||||||
terminal: Terminal | null
|
terminal: Terminal | null
|
||||||
}[]
|
}[]
|
||||||
setTerminals: React.Dispatch<React.SetStateAction<{
|
setTerminals: React.Dispatch<
|
||||||
id: string;
|
React.SetStateAction<
|
||||||
|
{
|
||||||
|
id: string
|
||||||
terminal: Terminal | null
|
terminal: Terminal | null
|
||||||
}[]>>
|
}[]
|
||||||
|
>
|
||||||
|
>
|
||||||
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
|
||||||
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
|
setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
|
||||||
socket: Socket
|
socket: Socket
|
||||||
activeTerminalId: string
|
activeTerminalId: string
|
||||||
}) => {
|
}) => {
|
||||||
const numTerminals = terminals.length;
|
const numTerminals = terminals.length
|
||||||
const index = terminals.findIndex((t) => t.id === term.id);
|
const index = terminals.findIndex((t) => t.id === term.id)
|
||||||
if (index === -1) return;
|
if (index === -1) return
|
||||||
|
|
||||||
setClosingTerminal(term.id);
|
setClosingTerminal(term.id)
|
||||||
|
|
||||||
socket.emit("closeTerminal", term.id, () => {
|
socket.emit("closeTerminal", term.id, () => {
|
||||||
setClosingTerminal("");
|
setClosingTerminal("")
|
||||||
|
|
||||||
const nextId =
|
const nextId =
|
||||||
activeTerminalId === term.id
|
activeTerminalId === term.id
|
||||||
@ -78,17 +85,17 @@ export const closeTerminal = ({
|
|||||||
: index < numTerminals - 1
|
: index < numTerminals - 1
|
||||||
? terminals[index + 1].id
|
? terminals[index + 1].id
|
||||||
: 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) {
|
if (!nextId) {
|
||||||
setActiveTerminalId("");
|
setActiveTerminalId("")
|
||||||
} else {
|
} else {
|
||||||
const nextTerminal = terminals.find((t) => t.id === nextId);
|
const nextTerminal = terminals.find((t) => t.id === nextId)
|
||||||
if (nextTerminal) {
|
if (nextTerminal) {
|
||||||
setActiveTerminalId(nextTerminal.id);
|
setActiveTerminalId(nextTerminal.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
@ -1,65 +1,65 @@
|
|||||||
// DB Types
|
// DB Types
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
email: string;
|
email: string
|
||||||
generations: number;
|
generations: number
|
||||||
sandbox: Sandbox[];
|
sandbox: Sandbox[]
|
||||||
usersToSandboxes: UsersToSandboxes[];
|
usersToSandboxes: UsersToSandboxes[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Sandbox = {
|
export type Sandbox = {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
type: string;
|
type: string
|
||||||
visibility: "public" | "private";
|
visibility: "public" | "private"
|
||||||
createdAt: Date;
|
createdAt: Date
|
||||||
userId: string;
|
userId: string
|
||||||
usersToSandboxes: UsersToSandboxes[];
|
usersToSandboxes: UsersToSandboxes[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type UsersToSandboxes = {
|
export type UsersToSandboxes = {
|
||||||
userId: string;
|
userId: string
|
||||||
sandboxId: string;
|
sandboxId: string
|
||||||
sharedOn: Date;
|
sharedOn: Date
|
||||||
};
|
}
|
||||||
|
|
||||||
export type R2Files = {
|
export type R2Files = {
|
||||||
objects: R2FileData[];
|
objects: R2FileData[]
|
||||||
truncated: boolean;
|
truncated: boolean
|
||||||
delimitedPrefixes: any[];
|
delimitedPrefixes: any[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type R2FileData = {
|
export type R2FileData = {
|
||||||
storageClass: string;
|
storageClass: string
|
||||||
uploaded: string;
|
uploaded: string
|
||||||
checksums: any;
|
checksums: any
|
||||||
httpEtag: string;
|
httpEtag: string
|
||||||
etag: string;
|
etag: string
|
||||||
size: number;
|
size: number
|
||||||
version: string;
|
version: string
|
||||||
key: string;
|
key: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TFolder = {
|
export type TFolder = {
|
||||||
id: string;
|
id: string
|
||||||
type: "folder";
|
type: "folder"
|
||||||
name: string;
|
name: string
|
||||||
children: (TFile | TFolder)[];
|
children: (TFile | TFolder)[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TFile = {
|
export type TFile = {
|
||||||
id: string;
|
id: string
|
||||||
type: "file";
|
type: "file"
|
||||||
name: string;
|
name: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TTab = TFile & {
|
export type TTab = TFile & {
|
||||||
saved: boolean;
|
saved: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export type TFileData = {
|
export type TFileData = {
|
||||||
id: string;
|
id: string
|
||||||
data: string;
|
data: string
|
||||||
};
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
// import { toast } from "sonner"
|
// import { toast } from "sonner"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { Sandbox, TFile, TFolder } from "./types"
|
|
||||||
import fileExtToLang from "./file-extension-to-language.json"
|
import fileExtToLang from "./file-extension-to-language.json"
|
||||||
|
import { Sandbox, TFile, TFolder } from "./types"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user