refactor(api): remove AI worker, add ai api route, add usage tiers
- Remove separate AI worker service - Added generation limits: FREE: 1000/month (For the beta version) PRO: 500/month ENTERPRISE: 1000/month - Integrate AI functionality into main API routes - Added monthly generations reset and usage tier upgrade API routes - Upgrade tier page to be added along with profile page section
This commit is contained in:
145
frontend/app/api/ai/route.ts
Normal file
145
frontend/app/api/ai/route.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const user = await currentUser()
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 })
|
||||
}
|
||||
|
||||
// Check and potentially reset monthly usage
|
||||
const resetResponse = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/check-reset`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ userId: user.id }),
|
||||
}
|
||||
)
|
||||
|
||||
if (!resetResponse.ok) {
|
||||
console.error("Failed to check usage reset")
|
||||
}
|
||||
|
||||
// Get user data and check tier
|
||||
const dbUser = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${user.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
const userData = await dbUser.json()
|
||||
|
||||
// Get tier settings
|
||||
const tierSettings = TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
||||
if (userData.generations >= tierSettings.generations) {
|
||||
return new Response(
|
||||
`AI generation limit reached for your ${userData.tier || "FREE"} tier`,
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
messages,
|
||||
context,
|
||||
activeFileContent,
|
||||
isEditMode,
|
||||
fileName,
|
||||
line
|
||||
} = await request.json()
|
||||
|
||||
// Create system message based on mode
|
||||
let systemMessage
|
||||
if (isEditMode) {
|
||||
systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code. If there is no code to modify, refer to the active file content and only output the code that is relevant to the user's instructions.
|
||||
|
||||
File: ${fileName}
|
||||
Line: ${line}
|
||||
|
||||
Context:
|
||||
${context || "No additional context provided"}
|
||||
|
||||
Active File Content:
|
||||
${activeFileContent}
|
||||
|
||||
Instructions: ${messages[0].content}
|
||||
|
||||
Respond only with the modified code that can directly replace the existing code.`
|
||||
} else {
|
||||
systemMessage = `You are an intelligent programming assistant. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
|
||||
|
||||
\`\`\`python
|
||||
print("Hello, World!")
|
||||
\`\`\`
|
||||
|
||||
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||
|
||||
${context ? `Context:\n${context}\n` : ""}
|
||||
${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
|
||||
}
|
||||
|
||||
// Create stream response
|
||||
const stream = await anthropic.messages.create({
|
||||
model: tierSettings.model,
|
||||
max_tokens: tierSettings.maxTokens,
|
||||
system: systemMessage,
|
||||
messages: messages.map((msg: { role: string; content: string }) => ({
|
||||
role: msg.role === "human" ? "user" : "assistant",
|
||||
content: msg.content,
|
||||
})),
|
||||
stream: true,
|
||||
})
|
||||
|
||||
// Increment user's generation count
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/increment-generations`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ userId: user.id }),
|
||||
}
|
||||
)
|
||||
|
||||
// Return streaming response
|
||||
const encoder = new TextEncoder()
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
||||
controller.enqueue(encoder.encode(chunk.delta.text))
|
||||
}
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("AI generation error:", error)
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "Internal Server Error",
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
42
frontend/app/api/user/upgrade-tier/route.ts
Normal file
42
frontend/app/api/user/upgrade-tier/route.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const user = await currentUser()
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 })
|
||||
}
|
||||
|
||||
const { tier } = await request.json()
|
||||
|
||||
// handle payment processing here
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/update-tier`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: user.id,
|
||||
tier,
|
||||
tierExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to upgrade tier")
|
||||
}
|
||||
|
||||
return new Response("Tier upgraded successfully")
|
||||
} catch (error) {
|
||||
console.error("Tier upgrade error:", error)
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "Internal Server Error",
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
@ -89,7 +89,8 @@ export const handleSend = async (
|
||||
setIsGenerating: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||
activeFileContent: string
|
||||
activeFileContent: string,
|
||||
isEditMode: boolean = false
|
||||
) => {
|
||||
// Return if input is empty and context is null
|
||||
if (input.trim() === "" && !context) return
|
||||
@ -129,17 +130,17 @@ export const handleSend = async (
|
||||
}))
|
||||
|
||||
// Fetch AI response for chat message component
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch("/api/ai",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
isEditMode: isEditMode,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
}
|
||||
@ -147,7 +148,8 @@ export const handleSend = async (
|
||||
|
||||
// Throw error if response is not ok
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to get AI response")
|
||||
const error = await response.text()
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
// Get reader for chat message component
|
||||
@ -197,7 +199,7 @@ export const handleSend = async (
|
||||
console.error("Error fetching AI response:", error)
|
||||
const errorMessage = {
|
||||
role: "assistant" as const,
|
||||
content: "Sorry, I encountered an error. Please try again.",
|
||||
content: error.message || "Sorry, I encountered an error. Please try again.",
|
||||
}
|
||||
setMessages((prev) => [...prev, errorMessage])
|
||||
}
|
||||
|
@ -5,14 +5,12 @@ import { Editor } from "@monaco-editor/react"
|
||||
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { Socket } from "socket.io-client"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "../ui/button"
|
||||
// import monaco from "monaco-editor"
|
||||
|
||||
export default function GenerateInput({
|
||||
user,
|
||||
socket,
|
||||
width,
|
||||
data,
|
||||
editor,
|
||||
@ -21,7 +19,6 @@ export default function GenerateInput({
|
||||
onClose,
|
||||
}: {
|
||||
user: User
|
||||
socket: Socket
|
||||
width: number
|
||||
data: {
|
||||
fileName: string
|
||||
@ -59,32 +56,54 @@ export default function GenerateInput({
|
||||
}: {
|
||||
regenerate?: boolean
|
||||
}) => {
|
||||
if (user.generations >= 1000) {
|
||||
toast.error("You reached the maximum # of generations.")
|
||||
return
|
||||
}
|
||||
try {
|
||||
setLoading({ generate: !regenerate, regenerate })
|
||||
setCurrentPrompt(input)
|
||||
|
||||
setLoading({ generate: !regenerate, regenerate })
|
||||
setCurrentPrompt(input)
|
||||
socket.emit(
|
||||
"generateCode",
|
||||
{
|
||||
fileName: data.fileName,
|
||||
code: data.code,
|
||||
line: data.line,
|
||||
instructions: regenerate ? currentPrompt : input,
|
||||
},
|
||||
(res: { response: string; success: boolean }) => {
|
||||
console.log("Generated code", res.response, res.success)
|
||||
// if (!res.success) {
|
||||
// toast.error("Failed to generate code.");
|
||||
// return;
|
||||
// }
|
||||
const response = await fetch("/api/ai", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: regenerate ? currentPrompt : input
|
||||
}],
|
||||
context: null,
|
||||
activeFileContent: data.code,
|
||||
isEditMode: true,
|
||||
fileName: data.fileName,
|
||||
line: data.line
|
||||
}),
|
||||
})
|
||||
|
||||
setCode(res.response)
|
||||
router.refresh()
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
toast.error(error)
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let result = ""
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
result += decoder.decode(value, { stream: true })
|
||||
}
|
||||
}
|
||||
|
||||
setCode(result.trim())
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error("Generation error:", error)
|
||||
toast.error("Failed to generate code")
|
||||
} finally {
|
||||
setLoading({ generate: false, regenerate: false })
|
||||
}
|
||||
}
|
||||
const handleGenerateForm = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
@ -949,7 +949,6 @@ export default function CodeEditor({
|
||||
{generate.show ? (
|
||||
<GenerateInput
|
||||
user={userData}
|
||||
socket={socket!}
|
||||
width={generate.width - 90}
|
||||
data={{
|
||||
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||
|
@ -9,22 +9,78 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { User } from "@/lib/types"
|
||||
import { useClerk } from "@clerk/nextjs"
|
||||
import { LogOut, Sparkles } from "lucide-react"
|
||||
import { Crown, LogOut, Sparkles } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import Avatar from "./avatar"
|
||||
import { Button } from "./button"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
|
||||
export default function UserButton({ userData }: { userData: User }) {
|
||||
if (!userData) return null
|
||||
// TODO: Remove this once we have a proper tier system
|
||||
const TIER_INFO = {
|
||||
FREE: {
|
||||
color: "text-gray-500",
|
||||
icon: Sparkles,
|
||||
limit: TIERS.FREE.generations,
|
||||
},
|
||||
PRO: {
|
||||
color: "text-blue-500",
|
||||
icon: Crown,
|
||||
limit: TIERS.PRO.generations,
|
||||
},
|
||||
ENTERPRISE: {
|
||||
color: "text-purple-500",
|
||||
icon: Crown,
|
||||
limit: TIERS.ENTERPRISE.generations,
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function UserButton({ userData: initialUserData }: { userData: User }) {
|
||||
const [userData, setUserData] = useState<User>(initialUserData)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { signOut } = useClerk()
|
||||
const router = useRouter()
|
||||
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${userData.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
cache: 'no-store'
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
const updatedUserData = await res.json()
|
||||
setUserData(updatedUserData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user data:", error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchUserData()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const tierInfo = TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
||||
const TierIcon = tierInfo.icon
|
||||
const usagePercentage = Math.floor((userData.generations || 0) * 100 / tierInfo.limit)
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
router.push('/upgrade')
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger>
|
||||
<Avatar name={userData.name} avatarUrl={userData.avatarUrl} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-48" align="end">
|
||||
<DropdownMenuContent className="w-64" 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">
|
||||
@ -33,20 +89,48 @@ export default function UserButton({ userData }: { userData: User }) {
|
||||
</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}/1000
|
||||
|
||||
<div className="py-1.5 px-2 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TierIcon className={`h-4 w-4 ${tierInfo.color}`} />
|
||||
<span className="text-sm font-medium">{userData.tier || "FREE"} Plan</span>
|
||||
</div>
|
||||
{/* {(userData.tier === "FREE" || userData.tier === "PRO") && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs border-b hover:border-b-foreground"
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
)} */}
|
||||
</div>
|
||||
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<div className="py-1.5 px-2 w-full">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>AI Usage</span>
|
||||
<span>{userData.generations}/{tierInfo.limit}</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary">
|
||||
<div
|
||||
className="h-full bg-indigo-500 rounded-full"
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
usagePercentage > 90 ? 'bg-red-500' :
|
||||
usagePercentage > 75 ? 'bg-yellow-500' :
|
||||
tierInfo.color.replace('text-', 'bg-')
|
||||
}`}
|
||||
style={{
|
||||
width: `${(userData.generations * 100) / 1000}%`,
|
||||
width: `${Math.min(usagePercentage, 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* <DropdownMenuItem className="cursor-pointer">
|
||||
@ -64,3 +148,4 @@ export default function UserButton({ userData }: { userData: User }) {
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
19
frontend/lib/tiers.ts
Normal file
19
frontend/lib/tiers.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export const TIERS = {
|
||||
FREE: {
|
||||
// generations: 100,
|
||||
// maxTokens: 1024,
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
PRO: {
|
||||
generations: 500,
|
||||
maxTokens: 2048,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
ENTERPRISE: {
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
}
|
@ -10,6 +10,9 @@ export type User = {
|
||||
generations: number
|
||||
sandbox: Sandbox[]
|
||||
usersToSandboxes: UsersToSandboxes[]
|
||||
tier: "FREE" | "PRO" | "ENTERPRISE"
|
||||
tierExpiresAt: Date
|
||||
lastResetDate?: number
|
||||
}
|
||||
|
||||
export type Sandbox = {
|
||||
|
119
frontend/package-lock.json
generated
119
frontend/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "sandbox",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.7",
|
||||
"@clerk/nextjs": "^4.29.12",
|
||||
"@clerk/themes": "^1.7.12",
|
||||
@ -124,6 +125,54 @@
|
||||
"nun": "bin/nun.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
|
||||
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||
"version": "18.19.65",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.65.tgz",
|
||||
"integrity": "sha512-Ay5BZuO1UkTmVHzZJNvZKw/E+iB3GQABb6kijEz89w2JrfhNA+M/ebp18pfz9Gqe9ywhMC8AA8yC01lZq48J+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.7.tgz",
|
||||
@ -3418,6 +3467,18 @@
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||
@ -3430,6 +3491,18 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
|
||||
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||
@ -4346,6 +4419,15 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
|
||||
@ -4506,6 +4588,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
@ -4514,6 +4602,28 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-node/node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
@ -4870,6 +4980,15 @@
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
@ -9,6 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.1.7",
|
||||
"@clerk/nextjs": "^4.29.12",
|
||||
"@clerk/themes": "^1.7.12",
|
||||
|
Reference in New Issue
Block a user