add ai generations limit + random bug fixes

This commit is contained in:
Ishaan Dey 2024-05-05 16:51:30 -07:00
parent 09ead6073b
commit 47ce5db223
20 changed files with 330 additions and 122 deletions

View File

@ -36,6 +36,13 @@
"when": 1714565073180,
"tag": "0004_cuddly_wolf_cub",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1714950365718,
"tag": "0005_last_the_twelve",
"breakpoints": true
}
]
}

View File

@ -5,7 +5,7 @@ import { ZodError, z } from "zod";
import { user, sandbox, usersToSandboxes } from "./schema";
import * as schema from "./schema";
import { and, eq } from "drizzle-orm";
import { and, eq, sql } from "drizzle-orm";
export interface Env {
DB: D1Database;
@ -86,7 +86,6 @@ export default {
const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get();
// console.log("sb:", sb);
await fetch("https://storage.ishaan1013.workers.dev/api/init", {
method: "POST",
body: JSON.stringify({ sandboxId: sb.id, type }),
@ -95,7 +94,6 @@ export default {
return new Response(sb.id, { status: 200 });
} else {
console.log(method);
return methodNotAllowed;
}
} else if (path === "/api/sandbox/share") {
@ -162,12 +160,35 @@ export default {
const body = await request.json();
const { sandboxId, userId } = deleteShareSchema.parse(body);
console.log("DELETE", sandboxId, userId);
await db.delete(usersToSandboxes).where(and(eq(usersToSandboxes.userId, userId), eq(usersToSandboxes.sandboxId, sandboxId)));
return success;
} else return methodNotAllowed;
} else if (path === "/api/sandbox/generate" && method === "POST") {
const generateSchema = z.object({
userId: z.string(),
});
const body = await request.json();
const { userId } = generateSchema.parse(body);
const dbUser = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, userId),
});
if (!dbUser) {
return new Response("User not found.", { status: 400 });
}
if (dbUser.generations !== null && dbUser.generations >= 30) {
return new Response("You reached the maximum # of generations.", { status: 400 });
}
await db
.update(user)
.set({ generations: sql`${user.generations} + 1` })
.where(eq(user.id, userId))
.get();
return success;
} else if (path === "/api/user") {
if (method === "GET") {
const params = url.searchParams;

View File

@ -10,6 +10,7 @@ export const user = sqliteTable("user", {
name: text("name").notNull(),
email: text("email").notNull(),
image: text("image"),
generations: integer("generations").default(0),
});
export type User = typeof user.$inferSelect;

View File

@ -1,6 +1,4 @@
---
# Credit: Harkirat Singh https://github.com/hkirat/repl
# Source: ingress-nginx/templates/controller-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount

View File

@ -65,14 +65,14 @@ io.use((socket, next) => __awaiter(void 0, void 0, void 0, function* () {
return;
}
socket.data = {
id: sandboxId,
userId,
sandboxId: sandboxId,
};
next();
}));
io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
const data = socket.data;
const sandboxFiles = yield (0, utils_1.getSandboxFiles)(data.id);
const sandboxFiles = yield (0, utils_1.getSandboxFiles)(data.sandboxId);
sandboxFiles.fileData.forEach((file) => {
const filePath = path_1.default.join(dirName, file.id);
fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
@ -113,7 +113,7 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
socket.on("createFile", (name) => __awaiter(void 0, void 0, void 0, function* () {
try {
yield ratelimit_1.createFileRL.consume(data.userId, 1);
const id = `projects/${data.id}/${name}`;
const id = `projects/${data.sandboxId}/${name}`;
fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) {
if (err)
throw err;
@ -165,7 +165,7 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
});
sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId);
yield (0, utils_1.deleteFile)(fileId);
const newFiles = yield (0, utils_1.getSandboxFiles)(data.id);
const newFiles = yield (0, utils_1.getSandboxFiles)(data.sandboxId);
callback(newFiles.files);
}
catch (e) {
@ -184,7 +184,7 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
const pty = (0, node_pty_1.spawn)(os_1.default.platform() === "win32" ? "cmd.exe" : "bash", [], {
name: "xterm",
cols: 100,
cwd: path_1.default.join(dirName, "projects", data.id),
cwd: path_1.default.join(dirName, "projects", data.sandboxId),
});
const onData = pty.onData((data) => {
socket.emit("terminalResponse", {
@ -213,37 +213,26 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
}
});
socket.on("generateCode", (fileName, code, line, instructions, callback) => __awaiter(void 0, void 0, void 0, function* () {
console.log("Generating code...");
const res = yield fetch(`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/llama-3-8b-instruct`, {
const fetchPromise = fetch(`http://localhost:8787/api/sandbox/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.CF_API_TOKEN}`,
},
body: JSON.stringify({
messages: [
{
role: "system",
content: "You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is correct, efficient, and follows best practices. You should also generate code that is clear and easy to read. When you generate code, you should only return the code, and nothing else. You should not include backticks in the code you generate.",
},
{
role: "user",
content: `The file is called ${fileName}.`,
},
{
role: "user",
content: `Here are my instructions on what to generate: ${instructions}.`,
},
{
role: "user",
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
${code}`,
},
],
userId: data.userId,
}),
});
const json = yield res.json();
const generateCodePromise = (0, utils_1.generateCode)({
fileName,
code,
line,
instructions,
});
const [fetchResponse, generateCodeResponse] = yield Promise.all([
fetchPromise,
generateCodePromise,
]);
const json = yield generateCodeResponse.json();
callback(json);
}));
socket.on("disconnect", () => {

View File

@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.deleteFile = exports.saveFile = exports.renameFile = exports.createFile = exports.getSandboxFiles = void 0;
exports.generateCode = exports.deleteFile = exports.saveFile = exports.renameFile = exports.createFile = exports.getSandboxFiles = void 0;
const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () {
const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`);
const sandboxData = yield sandboxRes.json();
@ -121,3 +121,35 @@ const deleteFile = (fileId) => __awaiter(void 0, void 0, void 0, function* () {
return res.ok;
});
exports.deleteFile = deleteFile;
const generateCode = (_a) => __awaiter(void 0, [_a], void 0, function* ({ fileName, code, line, instructions, }) {
return yield fetch(`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/llama-3-8b-instruct`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.CF_API_TOKEN}`,
},
body: JSON.stringify({
messages: [
{
role: "system",
content: "You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is correct, efficient, and follows best practices. You should also generate code that is clear and easy to read. When you generate code, you should only return the code, and nothing else. You should not include backticks in the code you generate.",
},
{
role: "user",
content: `The file is called ${fileName}.`,
},
{
role: "user",
content: `Here are my instructions on what to generate: ${instructions}.`,
},
{
role: "user",
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
${code}`,
},
],
}),
});
});
exports.generateCode = generateCode;

View File

@ -11,6 +11,7 @@ import { User } from "./types"
import {
createFile,
deleteFile,
generateCode,
getSandboxFiles,
renameFile,
saveFile,
@ -81,8 +82,8 @@ io.use(async (socket, next) => {
}
socket.data = {
id: sandboxId,
userId,
sandboxId: sandboxId,
}
next()
@ -91,10 +92,10 @@ io.use(async (socket, next) => {
io.on("connection", async (socket) => {
const data = socket.data as {
userId: string
id: string
sandboxId: string
}
const sandboxFiles = await getSandboxFiles(data.id)
const sandboxFiles = await getSandboxFiles(data.sandboxId)
sandboxFiles.fileData.forEach((file) => {
const filePath = path.join(dirName, file.id)
fs.mkdirSync(path.dirname(filePath), { recursive: true })
@ -142,7 +143,7 @@ io.on("connection", async (socket) => {
try {
await createFileRL.consume(data.userId, 1)
const id = `projects/${data.id}/${name}`
const id = `projects/${data.sandboxId}/${name}`
fs.writeFile(path.join(dirName, id), "", function (err) {
if (err) throw err
@ -206,7 +207,7 @@ io.on("connection", async (socket) => {
await deleteFile(fileId)
const newFiles = await getSandboxFiles(data.id)
const newFiles = await getSandboxFiles(data.sandboxId)
callback(newFiles.files)
} catch (e) {
socket.emit("rateLimit", "Rate limited: file deletion. Please slow down.")
@ -226,7 +227,7 @@ io.on("connection", async (socket) => {
const pty = spawn(os.platform() === "win32" ? "cmd.exe" : "bash", [], {
name: "xterm",
cols: 100,
cwd: path.join(dirName, "projects", data.id),
cwd: path.join(dirName, "projects", data.sandboxId),
})
const onData = pty.onData((data) => {
@ -269,42 +270,29 @@ io.on("connection", async (socket) => {
instructions: string,
callback
) => {
console.log("Generating code...")
const res = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/llama-3-8b-instruct`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.CF_API_TOKEN}`,
},
body: JSON.stringify({
messages: [
{
role: "system",
content:
"You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is correct, efficient, and follows best practices. You should also generate code that is clear and easy to read. When you generate code, you should only return the code, and nothing else. You should not include backticks in the code you generate.",
},
{
role: "user",
content: `The file is called ${fileName}.`,
},
{
role: "user",
content: `Here are my instructions on what to generate: ${instructions}.`,
},
{
role: "user",
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
${code}`,
},
],
}),
}
)
const fetchPromise = fetch(`http://localhost:8787/api/sandbox/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: data.userId,
}),
})
const json = await res.json()
const generateCodePromise = generateCode({
fileName,
code,
line,
instructions,
})
const [fetchResponse, generateCodeResponse] = await Promise.all([
fetchPromise,
generateCodePromise,
])
const json = await generateCodeResponse.json()
callback(json)
}
)

View File

@ -4,11 +4,9 @@ export type User = {
id: string
name: string
email: string
generations: number
sandbox: Sandbox[]
usersToSandboxes: {
userId: string
sandboxId: string
}[]
usersToSandboxes: UsersToSandboxes[]
}
export type Sandbox = {
@ -17,10 +15,12 @@ export type Sandbox = {
type: "react" | "node"
visibility: "public" | "private"
userId: string
usersToSandboxes: {
userId: string
sandboxId: string
}[]
usersToSandboxes: UsersToSandboxes[]
}
export type UsersToSandboxes = {
userId: string
sandboxId: string
}
export type TFolder = {

View File

@ -134,3 +134,49 @@ export const deleteFile = async (fileId: string) => {
})
return res.ok
}
export const generateCode = async ({
fileName,
code,
line,
instructions,
}: {
fileName: string
code: string
line: number
instructions: string
}) => {
return await fetch(
`https://api.cloudflare.com/client/v4/accounts/${process.env.CF_USER_ID}/ai/run/@cf/meta/llama-3-8b-instruct`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.CF_API_TOKEN}`,
},
body: JSON.stringify({
messages: [
{
role: "system",
content:
"You are an expert coding assistant. You read code from a file, and you suggest new code to add to the file. You may be given instructions on what to generate, which you should follow. You should generate code that is correct, efficient, and follows best practices. You should also generate code that is clear and easy to read. When you generate code, you should only return the code, and nothing else. You should not include backticks in the code you generate.",
},
{
role: "user",
content: `The file is called ${fileName}.`,
},
{
role: "user",
content: `Here are my instructions on what to generate: ${instructions}.`,
},
{
role: "user",
content: `Suggest me code to insert at line ${line} in my file. Give only the code, and NOTHING else. DO NOT include backticks in your response. My code file content is as follows
${code}`,
},
],
}),
}
)
}

View File

@ -5,9 +5,13 @@ import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client"
import { Editor } from "@monaco-editor/react"
import { User } from "@/lib/types"
import { toast } from "sonner"
import { usePathname, useRouter } from "next/navigation"
// import monaco from "monaco-editor"
export default function GenerateInput({
user,
socket,
width,
data,
@ -15,6 +19,7 @@ export default function GenerateInput({
onExpand,
onAccept,
}: {
user: User
socket: Socket
width: number
data: {
@ -28,6 +33,8 @@ export default function GenerateInput({
onExpand: () => void
onAccept: (code: string) => void
}) {
const pathname = usePathname()
const router = useRouter()
const inputRef = useRef<HTMLInputElement>(null)
const [code, setCode] = useState("")
@ -50,6 +57,12 @@ export default function GenerateInput({
}: {
regenerate?: boolean
}) => {
if (user.generations >= 30) {
toast.error(
"You reached the maximum # of generations. Contact @ishaandey_ on X/Twitter to reset :)"
)
}
setLoading({ generate: !regenerate, regenerate })
setCurrentPrompt(input)
socket.emit(
@ -72,6 +85,7 @@ export default function GenerateInput({
}
setCode(res.result.response)
router.refresh()
}
)
}

View File

@ -34,8 +34,7 @@ import Sidebar from "./sidebar"
import EditorTerminal from "./terminal"
import { Button } from "../ui/button"
import GenerateInput from "./generate"
import { TFile, TFileData, TFolder, TTab } from "./sidebar/types"
import { Sandbox, User } from "@/lib/types"
import { Sandbox, User, TFile, TFileData, TFolder, TTab } from "@/lib/types"
import { processFileType, validateName } from "@/lib/utils"
import { Cursors } from "./live/cursors"
@ -455,6 +454,7 @@ export default function CodeEditor({
<div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? (
<GenerateInput
user={userData}
socket={socket}
width={generate.width - 90}
data={{

View File

@ -2,7 +2,7 @@
import Image from "next/image"
import { getIconForFile } from "vscode-icons-js"
import { TFile, TTab } from "./types"
import { TFile, TTab } from "@/lib/types"
import { useEffect, useRef, useState } from "react"
import {
ContextMenu,

View File

@ -3,7 +3,7 @@
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import { TFile, TFolder, TTab } from "./types"
import { TFile, TFolder, TTab } from "@/lib/types"
import SidebarFile from "./file"
import {
ContextMenu,

View File

@ -3,7 +3,7 @@
import { FilePlus, FolderPlus, Loader2, Search, Sparkles } from "lucide-react"
import SidebarFile from "./file"
import SidebarFolder from "./folder"
import { TFile, TFolder, TTab } from "./types"
import { TFile, TFolder, TTab } from "@/lib/types"
import { useState } from "react"
import New from "./new"
import { Socket } from "socket.io-client"

View File

@ -1,21 +0,0 @@
export type TFolder = {
id: string
type: "folder"
name: string
children: (TFile | TFolder)[]
}
export type TFile = {
id: string
type: "file"
name: string
}
export type TTab = TFile & {
saved: boolean
}
export type TFileData = {
id: string
data: string
}

View File

@ -10,7 +10,7 @@ import {
} from "@/components/ui/dropdown-menu"
import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs"
import { LogOut, Pencil } from "lucide-react"
import { LogOut, Pencil, Sparkles } from "lucide-react"
import { useRouter } from "next/navigation"
export default function UserButton({ userData }: { userData: User }) {
@ -29,7 +29,7 @@ export default function UserButton({ userData }: { userData: User }) {
.map((name) => name[0].toUpperCase())}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuContent className="w-48" align="end">
<div className="py-1.5 px-2 w-full">
<div className="font-medium">{userData.name}</div>
<div className="text-sm w-full overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
@ -37,6 +37,21 @@ export default function UserButton({ userData }: { userData: User }) {
</div>
</div>
<DropdownMenuSeparator />
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
<div className="flex items-center">
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
AI Usage: {userData.generations}/30
</div>
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
<div
className="h-full bg-indigo-500 rounded-full"
style={{
width: `${(userData.generations * 100) / 30}%`,
}}
/>
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer">
<Pencil className="mr-2 h-4 w-4" />

View File

@ -4,6 +4,7 @@ export type User = {
id: string
name: string
email: string
generations: number
sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[]
}
@ -38,3 +39,25 @@ export type R2FileData = {
version: string
key: string
}
export type TFolder = {
id: string
type: "folder"
name: string
children: (TFile | TFolder)[]
}
export type TFile = {
id: string
type: "file"
name: string
}
export type TTab = TFile & {
saved: boolean
}
export type TFileData = {
id: string
data: string
}

View File

@ -54,6 +54,8 @@
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.2",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
@ -247,6 +249,50 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
},
"node_modules/@csstools/selector-resolve-nested": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz",
"integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
"postcss-selector-parser": "^6.0.13"
}
},
"node_modules/@csstools/selector-specificity": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
"integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
"postcss-selector-parser": "^6.0.13"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
@ -2810,16 +2856,17 @@
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz",
"integrity": "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==",
"dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
@ -2906,6 +2953,33 @@
"postcss": "^8.2.14"
}
},
"node_modules/postcss-nesting": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.2.tgz",
"integrity": "sha512-FUmTHGDNundodutB4PUBxt/EPuhgtpk8FJGRsBhOuy+6FnkR2A8RZWIsyyy6XmhvX2DZQQWIkvu+HB4IbJm+Ew==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"dependencies": {
"@csstools/selector-resolve-nested": "^1.1.0",
"@csstools/selector-specificity": "^3.0.3",
"postcss-selector-parser": "^6.0.13"
},
"engines": {
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
"postcss": "^8.4"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
@ -3476,6 +3550,22 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/tailwindcss/node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",

View File

@ -55,6 +55,8 @@
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.2",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}

View File

@ -1,6 +1,9 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
plugins: [
"postcss-import",
"tailwindcss/nesting",
"postcss-nesting",
"autoprefixer",
"tailwindcss",
],
}