diff --git a/backend/server/dist/index.js b/backend/server/dist/index.js
index 2134505..747f7ae 100644
--- a/backend/server/dist/index.js
+++ b/backend/server/dist/index.js
@@ -22,6 +22,7 @@ const socket_io_1 = require("socket.io");
const zod_1 = require("zod");
const utils_1 = require("./utils");
const node_pty_1 = require("node-pty");
+const ratelimit_1 = require("./ratelimit");
dotenv_1.default.config();
const app = (0, express_1.default)();
const port = process.env.PORT || 4000;
@@ -89,60 +90,97 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
});
// todo: send diffs + debounce for efficiency
socket.on("saveFile", (fileId, body) => __awaiter(void 0, void 0, void 0, function* () {
- const file = sandboxFiles.fileData.find((f) => f.id === fileId);
- if (!file)
- return;
- file.data = body;
- fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) {
- if (err)
- throw err;
- });
- yield (0, utils_1.saveFile)(fileId, body);
+ try {
+ yield ratelimit_1.saveFileRL.consume(data.userId, 1);
+ if (Buffer.byteLength(body, "utf-8") > ratelimit_1.MAX_BODY_SIZE) {
+ socket.emit("rateLimit", "Rate limited: file size too large. Please reduce the file size.");
+ return;
+ }
+ const file = sandboxFiles.fileData.find((f) => f.id === fileId);
+ if (!file)
+ return;
+ file.data = body;
+ fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) {
+ if (err)
+ throw err;
+ });
+ yield (0, utils_1.saveFile)(fileId, body);
+ }
+ catch (e) {
+ socket.emit("rateLimit", "Rate limited: file saving. Please slow down.");
+ }
}));
socket.on("createFile", (name) => __awaiter(void 0, void 0, void 0, function* () {
- const id = `projects/${data.id}/${name}`;
- fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) {
- if (err)
- throw err;
- });
- sandboxFiles.files.push({
- id,
- name,
- type: "file",
- });
- sandboxFiles.fileData.push({
- id,
- data: "",
- });
- yield (0, utils_1.createFile)(id);
+ try {
+ yield ratelimit_1.createFileRL.consume(data.userId, 1);
+ const id = `projects/${data.id}/${name}`;
+ fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) {
+ if (err)
+ throw err;
+ });
+ sandboxFiles.files.push({
+ id,
+ name,
+ type: "file",
+ });
+ sandboxFiles.fileData.push({
+ id,
+ data: "",
+ });
+ yield (0, utils_1.createFile)(id);
+ }
+ catch (e) {
+ socket.emit("rateLimit", "Rate limited: file creation. Please slow down.");
+ }
}));
socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () {
- const file = sandboxFiles.fileData.find((f) => f.id === fileId);
- if (!file)
+ try {
+ yield ratelimit_1.renameFileRL.consume(data.userId, 1);
+ const file = sandboxFiles.fileData.find((f) => f.id === fileId);
+ if (!file)
+ return;
+ file.id = newName;
+ const parts = fileId.split("/");
+ const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName;
+ fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) {
+ if (err)
+ throw err;
+ });
+ yield (0, utils_1.renameFile)(fileId, newFileId, file.data);
+ }
+ catch (e) {
+ socket.emit("rateLimit", "Rate limited: file renaming. Please slow down.");
return;
- file.id = newName;
- const parts = fileId.split("/");
- const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName;
- fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) {
- if (err)
- throw err;
- });
- yield (0, utils_1.renameFile)(fileId, newFileId, file.data);
+ }
}));
socket.on("deleteFile", (fileId, callback) => __awaiter(void 0, void 0, void 0, function* () {
- const file = sandboxFiles.fileData.find((f) => f.id === fileId);
- if (!file)
- return;
- fs_1.default.unlink(path_1.default.join(dirName, fileId), function (err) {
- if (err)
- throw err;
- });
- sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId);
- yield (0, utils_1.deleteFile)(fileId);
- const newFiles = yield (0, utils_1.getSandboxFiles)(data.id);
- callback(newFiles.files);
+ try {
+ yield ratelimit_1.deleteFileRL.consume(data.userId, 1);
+ const file = sandboxFiles.fileData.find((f) => f.id === fileId);
+ if (!file)
+ return;
+ fs_1.default.unlink(path_1.default.join(dirName, fileId), function (err) {
+ if (err)
+ throw err;
+ });
+ sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId);
+ yield (0, utils_1.deleteFile)(fileId);
+ const newFiles = yield (0, utils_1.getSandboxFiles)(data.id);
+ callback(newFiles.files);
+ }
+ catch (e) {
+ socket.emit("rateLimit", "Rate limited: file deletion. Please slow down.");
+ }
}));
socket.on("createTerminal", ({ id }) => {
+ if (terminals[id]) {
+ console.log("Terminal already exists.");
+ return;
+ }
+ if (Object.keys(terminals).length >= 4) {
+ console.log("Too many terminals.");
+ return;
+ }
const pty = (0, node_pty_1.spawn)(os_1.default.platform() === "win32" ? "cmd.exe" : "bash", [], {
name: "xterm",
cols: 100,
diff --git a/frontend/app/api/lb-auth/route.ts b/frontend/app/api/lb-auth/route.ts
index ad480bc..08dfcfb 100644
--- a/frontend/app/api/lb-auth/route.ts
+++ b/frontend/app/api/lb-auth/route.ts
@@ -1,3 +1,4 @@
+import { colors } from "@/lib/colors"
import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { Liveblocks } from "@liveblocks/node"
@@ -19,12 +20,19 @@ export async function POST(request: NextRequest) {
const res = await fetch(`http://localhost:8787/api/user?id=${clerkUser.id}`)
const user = (await res.json()) as User
+ const colorNames = Object.keys(colors)
+ const randomColor = colorNames[
+ Math.floor(Math.random() * colorNames.length)
+ ] as keyof typeof colors
+ const code = colors[randomColor]
+
// Create a session for the current user
// userInfo is made available in Liveblocks presence hooks, e.g. useOthers
const session = liveblocks.prepareSession(user.id, {
userInfo: {
name: user.name,
email: user.email,
+ color: randomColor,
},
})
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index abbbb13..aa82857 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -99,13 +99,23 @@
background: radial-gradient(circle at bottom right, #312e81 -75%, hsl(0 0% 3.9%) 60%); /* violet 900 -> bg */
}
+.inline-decoration::before {
+ content: 'Generate';
+ color: #525252;
+ /* border: 1px solid #525252; */
+ /* padding: 2px 4px; */
+ /* border-radius: 4px; */
+ margin-left: 36px;
+}
.inline-decoration::after {
- content: 'Generate ⌘G';
+ content: '⌘G';
color: #525252;
border: 1px solid #525252;
- padding: 2px 4px;
+ border-bottom-width: 2px;
+ padding: 0 4px;
border-radius: 4px;
- margin-left: 56px;
+ margin-left: 6px;
+ line-height: 0;
}
.yRemoteSelection {
diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx
index 3bf7b7b..908d58b 100644
--- a/frontend/components/editor/index.tsx
+++ b/frontend/components/editor/index.tsx
@@ -11,12 +11,7 @@ import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness"
-import {
- TypedLiveblocksProvider,
- useRoom,
- AwarenessList,
- useSelf,
-} from "@/liveblocks.config"
+import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"
import {
ResizableHandle,
@@ -27,6 +22,7 @@ import {
ChevronLeft,
ChevronRight,
FileJson,
+ Loader2,
Plus,
RotateCw,
Shell,
@@ -355,15 +351,20 @@ export default function CodeEditor({
setFiles(files)
}
- socket.on("connect", onConnect)
+ const onRateLimit = (message: string) => {
+ toast.error(message)
+ }
+ socket.on("connect", onConnect)
socket.on("disconnect", onDisconnect)
socket.on("loaded", onLoadedEvent)
+ socket.on("rateLimit", onRateLimit)
return () => {
socket.off("connect", onConnect)
socket.off("disconnect", onDisconnect)
socket.off("loaded", onLoadedEvent)
+ socket.off("rateLimit", onRateLimit)
}
}, [])
@@ -547,7 +548,7 @@ export default function CodeEditor({
>
{!activeId ? (
<>
-
+
No file selected.
@@ -591,7 +592,12 @@ export default function CodeEditor({
value={activeFile ?? ""}
/>
>
- ) : null}
+ ) : (
+
+
+ Waiting for Clerk to load...
+
+ )}
@@ -629,27 +635,36 @@ export default function CodeEditor({
minSize={20}
className="p-2 flex flex-col"
>
-
-
-
- Shell
-
-
-
-
- {socket ? : null}
-
+ {isOwner ? (
+ <>
+
+
+
+ Shell
+
+
+
+
+ {socket ? : null}
+
+ >
+ ) : (
+
+
+ No terminal access.
+
+ )}
diff --git a/frontend/components/editor/live/avatars.tsx b/frontend/components/editor/live/avatars.tsx
index 92c72ff..f52bc14 100644
--- a/frontend/components/editor/live/avatars.tsx
+++ b/frontend/components/editor/live/avatars.tsx
@@ -1,33 +1,39 @@
"use client"
-import { colorClasses, colors } from "@/lib/colors"
import { useOthers } from "@/liveblocks.config"
-import { useState } from "react"
+
+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",
+ orange:
+ "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-orange-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-orange-950 to-orange-600 flex items-center justify-center text-xs font-medium",
+ yellow:
+ "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-yellow-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-yellow-950 to-yellow-600 flex items-center justify-center text-xs font-medium",
+ green:
+ "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-green-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-green-950 to-green-600 flex items-center justify-center text-xs font-medium",
+ blue: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-blue-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-blue-950 to-blue-600 flex items-center justify-center text-xs font-medium",
+ purple:
+ "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium",
+ pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium",
+}
export function Avatars() {
const users = useOthers()
- const colorNames = Object.keys(colors)
- const [activeColors, setActiveColors] = useState([])
-
return (
-
- {users.map(({ connectionId, info }) => {
- const c = colorNames[
- connectionId % colorNames.length
- ] as keyof typeof colors
-
- return (
-
- {info.name
- .split(" ")
- .slice(0, 2)
- .map((letter) => letter[0].toUpperCase())}
-
- )
- })}
-
+ <>
+
+ {users.map(({ connectionId, info }) => {
+ return (
+
+ {info.name
+ .split(" ")
+ .slice(0, 2)
+ .map((letter) => letter[0].toUpperCase())}
+
+ )
+ })}
+
+
+ >
)
}
diff --git a/frontend/components/editor/live/cursors.tsx b/frontend/components/editor/live/cursors.tsx
index c86dd47..18d3fa0 100644
--- a/frontend/components/editor/live/cursors.tsx
+++ b/frontend/components/editor/live/cursors.tsx
@@ -5,11 +5,14 @@ import {
UserAwareness,
useSelf,
} from "@/liveblocks.config"
+import { colors } from "@/lib/colors"
export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
// Get user info from Liveblocks authentication endpoint
const userInfo = useSelf((me) => me.info)
+ if (!userInfo) return null
+
const [awarenessUsers, setAwarenessUsers] = useState([])
useEffect(() => {
@@ -40,7 +43,7 @@ export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
cursorStyles += `
.yRemoteSelection-${clientId},
.yRemoteSelectionHead-${clientId} {
- --user-color: ${"#FF0000"};
+ --user-color: ${colors[client.user.color]};
}
.yRemoteSelectionHead-${clientId}::after {
diff --git a/frontend/components/editor/live/room.tsx b/frontend/components/editor/live/room.tsx
index 5ba17c5..a59aaf3 100644
--- a/frontend/components/editor/live/room.tsx
+++ b/frontend/components/editor/live/room.tsx
@@ -2,6 +2,7 @@
import { RoomProvider } from "@/liveblocks.config"
import { ClientSideSuspense } from "@liveblocks/react"
+import Loading from "../loading"
export function Room({
id,
@@ -17,9 +18,9 @@ export function Room({
cursor: null,
}}
>
- Loading!!!!}>
- {() => children}
-
+ {/* }> */}
+ {children}
+ {/* */}
)
}
diff --git a/frontend/components/editor/loading.tsx b/frontend/components/editor/loading.tsx
new file mode 100644
index 0000000..6780701
--- /dev/null
+++ b/frontend/components/editor/loading.tsx
@@ -0,0 +1,41 @@
+import Image from "next/image"
+import Logo from "@/assets/logo.svg"
+import { Skeleton } from "../ui/skeleton"
+import { Loader, Loader2 } from "lucide-react"
+
+export default function Loading() {
+ return (
+
+
+
+
+
+
+ Loading...
+
{" "}
+
+
+ )
+}
diff --git a/frontend/components/editor/navbar/index.tsx b/frontend/components/editor/navbar/index.tsx
index 014c0a1..b5cfede 100644
--- a/frontend/components/editor/navbar/index.tsx
+++ b/frontend/components/editor/navbar/index.tsx
@@ -62,7 +62,7 @@ export default function Navbar({
) : null}
-
+
{isOwner ? (
diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx
index e80984e..e569e61 100644
--- a/frontend/components/editor/sidebar/index.tsx
+++ b/frontend/components/editor/sidebar/index.tsx
@@ -8,7 +8,7 @@ import { useState } from "react"
import New from "./new"
import { Socket } from "socket.io-client"
import Button from "@/components/ui/customButton"
-import Toggle from "@/components/ui/customToggle"
+import { Switch } from "@/components/ui/switch"
export default function Sidebar({
files,
@@ -104,12 +104,20 @@ export default function Sidebar({
)}
- {/* */}
-
-
- AI Copilot
-
- {/*
*/}
+
+
+
+ Copilot{" "}
+
+ ⌘G
+
+
+
+
)
}
diff --git a/frontend/components/ui/customToggle.tsx b/frontend/components/ui/customToggle.tsx
deleted file mode 100644
index 776b6bf..0000000
--- a/frontend/components/ui/customToggle.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as React from "react"
-import { Plus } from "lucide-react"
-import { cn } from "@/lib/utils"
-import { Button } from "./button"
-
-const Toggle = ({
- children,
- className,
- value,
- setValue,
-}: {
- children: React.ReactNode
- className?: string
- value: boolean
- setValue: React.Dispatch>
-}) => {
- if (value)
- return (
-
- )
- else
- return (
-
- )
-}
-
-export default Toggle
diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx
new file mode 100644
index 0000000..d7e45f7
--- /dev/null
+++ b/frontend/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/frontend/components/ui/switch.tsx b/frontend/components/ui/switch.tsx
new file mode 100644
index 0000000..5f4117f
--- /dev/null
+++ b/frontend/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/frontend/lib/colors.ts b/frontend/lib/colors.ts
index 8967c38..e30f754 100644
--- a/frontend/lib/colors.ts
+++ b/frontend/lib/colors.ts
@@ -7,34 +7,3 @@ export const colors = {
purple: "#7c3aed",
pink: "#db2777",
}
-
-export const colorClasses = {
- red: {
- ring: "ring-red-700",
- bg: "from-red-950 to-red-600",
- },
- orange: {
- ring: "ring-orange-700",
- bg: "from-orange-950 to-orange-600",
- },
- yellow: {
- ring: "ring-yellow-700",
- bg: "from-yellow-950 to-yellow-600",
- },
- green: {
- ring: "ring-green-700",
- bg: "from-green-950 to-green-600",
- },
- blue: {
- ring: "ring-blue-700",
- bg: "from-blue-950 to-blue-600",
- },
- purple: {
- ring: "ring-purple-700",
- bg: "from-purple-950 to-purple-600",
- },
- pink: {
- ring: "ring-pink-700",
- bg: "from-pink-950 to-pink-600",
- },
-}
diff --git a/frontend/liveblocks.config.ts b/frontend/liveblocks.config.ts
index b898d9e..df22c32 100644
--- a/frontend/liveblocks.config.ts
+++ b/frontend/liveblocks.config.ts
@@ -1,6 +1,7 @@
import { createClient } from "@liveblocks/client"
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react"
import YLiveblocksProvider from "@liveblocks/yjs"
+import { colors } from "./lib/colors"
const client = createClient({
// publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
@@ -73,6 +74,7 @@ type UserMeta = {
info: {
name: string
email: string
+ color: keyof typeof colors
}
}
@@ -99,60 +101,56 @@ export type AwarenessList = [number, UserAwareness][]
// Room-level hooks, use inside `RoomProvider`
export const {
- suspense: {
- RoomProvider,
- useRoom,
- useMyPresence,
- useUpdateMyPresence,
- useSelf,
- useOthers,
- useOthersMapped,
- useOthersListener,
- useOthersConnectionIds,
- useOther,
- useBroadcastEvent,
- useEventListener,
- useErrorListener,
- useStorage,
- useBatch,
- useHistory,
- useUndo,
- useRedo,
- useCanUndo,
- useCanRedo,
- useMutation,
- useStatus,
- useLostConnectionListener,
- useThreads,
- useCreateThread,
- useEditThreadMetadata,
- useCreateComment,
- useEditComment,
- useDeleteComment,
- useAddReaction,
- useRemoveReaction,
- useThreadSubscription,
- useMarkThreadAsRead,
- useRoomNotificationSettings,
- useUpdateRoomNotificationSettings,
+ RoomProvider,
+ useRoom,
+ useMyPresence,
+ useUpdateMyPresence,
+ useSelf,
+ useOthers,
+ useOthersMapped,
+ useOthersListener,
+ useOthersConnectionIds,
+ useOther,
+ useBroadcastEvent,
+ useEventListener,
+ useErrorListener,
+ useStorage,
+ useBatch,
+ useHistory,
+ useUndo,
+ useRedo,
+ useCanUndo,
+ useCanRedo,
+ useMutation,
+ useStatus,
+ useLostConnectionListener,
+ useThreads,
+ useCreateThread,
+ useEditThreadMetadata,
+ useCreateComment,
+ useEditComment,
+ useDeleteComment,
+ useAddReaction,
+ useRemoveReaction,
+ useThreadSubscription,
+ useMarkThreadAsRead,
+ useRoomNotificationSettings,
+ useUpdateRoomNotificationSettings,
- // These hooks can be exported from either context
- useUser,
- useRoomInfo,
- },
+ // These hooks can be exported from either context
+ useUser,
+ useRoomInfo,
} = createRoomContext(
client
)
// Project-level hooks, use inside `LiveblocksProvider`
export const {
- suspense: {
- LiveblocksProvider,
- useMarkInboxNotificationAsRead,
- useMarkAllInboxNotificationsAsRead,
- useInboxNotifications,
- useUnreadInboxNotificationsCount,
- },
+ LiveblocksProvider,
+ useMarkInboxNotificationAsRead,
+ useMarkAllInboxNotificationsAsRead,
+ useInboxNotifications,
+ useUnreadInboxNotificationsCount,
} = createLiveblocksContext(client)
export type TypedLiveblocksProvider = YLiveblocksProvider<
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ac33b95..8061baa 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -24,6 +24,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.0.3",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.0",
@@ -1228,6 +1229,35 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
+ "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "@radix-ui/react-use-previous": "1.0.1",
+ "@radix-ui/react-use-size": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index e201b66..016b772 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -25,6 +25,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.0.3",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.0",