Merge branch 'main' into feat/profile-page
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
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"
|
||||
@ -51,7 +51,11 @@ const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
|
||||
}
|
||||
)
|
||||
const userData: User = await userRes.json()
|
||||
return { id: userData.id, name: userData.name, avatarUrl: userData.avatarUrl }
|
||||
return {
|
||||
id: userData.id,
|
||||
name: userData.name,
|
||||
avatarUrl: userData.avatarUrl,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@ -89,18 +93,20 @@ export default async function CodePage({ params }: { params: { id: string } }) {
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||
<Room id={sandboxId}>
|
||||
{/* <Room id={sandboxId}> */}
|
||||
<TerminalProvider>
|
||||
<Navbar
|
||||
userData={userData}
|
||||
sandboxData={sandboxData}
|
||||
shared={shared}
|
||||
shared={
|
||||
shared as { id: string; name: string; avatarUrl: string }[]
|
||||
}
|
||||
/>
|
||||
<div className="w-screen flex grow">
|
||||
<CodeEditor userData={userData} sandboxData={sandboxData} />
|
||||
</div>
|
||||
</TerminalProvider>
|
||||
</Room>
|
||||
{/* </Room> */}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
171
frontend/app/api/ai/route.ts
Normal file
171
frontend/app/api/ai/route.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
import { templateConfigs } from "@/lib/templates"
|
||||
|
||||
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,
|
||||
templateType
|
||||
} = await request.json()
|
||||
|
||||
// Get template configuration
|
||||
const templateConfig = templateConfigs[templateType]
|
||||
|
||||
// Create template context
|
||||
const templateContext = templateConfig ? `
|
||||
Project Template: ${templateConfig.name}
|
||||
|
||||
File Structure:
|
||||
${Object.entries(templateConfig.fileStructure)
|
||||
.map(([path, info]) => `${path} - ${info.description}`)
|
||||
.join('\n')}
|
||||
|
||||
Conventions:
|
||||
${templateConfig.conventions.join('\n')}
|
||||
|
||||
Dependencies:
|
||||
${JSON.stringify(templateConfig.dependencies, null, 2)}
|
||||
` : ''
|
||||
|
||||
// Create system message based on mode
|
||||
let systemMessage
|
||||
if (isEditMode) {
|
||||
systemMessage = `You are an AI code editor working in a ${templateType} project. 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.
|
||||
|
||||
${templateContext}
|
||||
|
||||
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 for a ${templateType} project. 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.
|
||||
|
||||
This is the project template:
|
||||
${templateContext}
|
||||
|
||||
${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 }
|
||||
)
|
||||
}
|
||||
}
|
@ -1,57 +1,61 @@
|
||||
import { colors } from "@/lib/colors"
|
||||
import { User } from "@/lib/types"
|
||||
// import { colors } from "@/lib/colors"
|
||||
// import { User } from "@/lib/types"
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
import { Liveblocks } from "@liveblocks/node"
|
||||
// import { Liveblocks } from "@liveblocks/node"
|
||||
import { NextRequest } from "next/server"
|
||||
|
||||
const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY!
|
||||
// const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY!
|
||||
|
||||
const liveblocks = new Liveblocks({
|
||||
secret: API_KEY!,
|
||||
})
|
||||
// const liveblocks = new Liveblocks({
|
||||
// secret: API_KEY!,
|
||||
// })
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const clerkUser = await currentUser()
|
||||
// Temporarily return unauthorized while Liveblocks is disabled
|
||||
return new Response("Liveblocks collaboration temporarily disabled", { status: 503 })
|
||||
|
||||
if (!clerkUser) {
|
||||
return new Response("Unauthorized", { status: 401 })
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${clerkUser.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
// Give the user access to the room
|
||||
user.sandbox.forEach((sandbox) => {
|
||||
session.allow(`${sandbox.id}`, session.FULL_ACCESS)
|
||||
})
|
||||
user.usersToSandboxes.forEach((userToSandbox) => {
|
||||
session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS)
|
||||
})
|
||||
|
||||
// Authorize the user and return the result
|
||||
const { body, status } = await session.authorize()
|
||||
return new Response(body, { status })
|
||||
// Original implementation commented out:
|
||||
// const clerkUser = await currentUser()
|
||||
//
|
||||
// if (!clerkUser) {
|
||||
// return new Response("Unauthorized", { status: 401 })
|
||||
// }
|
||||
//
|
||||
// const res = await fetch(
|
||||
// `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${clerkUser.id}`,
|
||||
// {
|
||||
// headers: {
|
||||
// Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
// },
|
||||
// }
|
||||
// )
|
||||
// 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,
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// // Give the user access to the room
|
||||
// user.sandbox.forEach((sandbox) => {
|
||||
// session.allow(`${sandbox.id}`, session.FULL_ACCESS)
|
||||
// })
|
||||
// user.usersToSandboxes.forEach((userToSandbox) => {
|
||||
// session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS)
|
||||
// })
|
||||
//
|
||||
// // Authorize the user and return the result
|
||||
// const { body, status } = await session.authorize()
|
||||
// return new Response(body, { status })
|
||||
}
|
||||
|
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 }
|
||||
)
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { ThemeProvider } from "@/components/ui/theme-provider"
|
||||
import { PreviewProvider } from "@/context/PreviewContext"
|
||||
import { SocketProvider } from '@/context/SocketContext'
|
||||
import { SocketProvider } from "@/context/SocketContext"
|
||||
import { ClerkProvider } from "@clerk/nextjs"
|
||||
import { Analytics } from "@vercel/analytics/react"
|
||||
import { GeistMono } from "geist/font/mono"
|
||||
|
@ -14,4 +14,4 @@
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,38 @@ export default function AboutModal({
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>About this project</DialogTitle>
|
||||
<DialogTitle>Help & Support</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Sandbox is an open-source cloud-based code editing environment with
|
||||
custom AI code autocompletion and real-time collaboration.
|
||||
<div className="space-y-4">
|
||||
{/* <div className="text-sm text-muted-foreground">
|
||||
Sandbox is an open-source cloud-based code editing environment with
|
||||
custom AI code autocompletion and real-time collaboration.
|
||||
</div> */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Get help and support through our Discord community or by creating issues on GitHub:
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm">
|
||||
<a
|
||||
href="https://discord.gitwit.dev/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Join our Discord community →
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<a
|
||||
href="https://github.com/jamesmurdza/sandbox/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Report issues on GitHub →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -25,6 +25,7 @@ export default function Dashboard({
|
||||
type: "react" | "node"
|
||||
author: string
|
||||
sharedOn: Date
|
||||
authorAvatarUrl?: string
|
||||
}[]
|
||||
}) {
|
||||
const [screen, setScreen] = useState<TScreen>("projects")
|
||||
@ -77,14 +78,14 @@ export default function Dashboard({
|
||||
<FolderDot className="w-4 h-4 mr-2" />
|
||||
My Projects
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="ghost"
|
||||
onClick={() => setScreen("shared")}
|
||||
className={activeScreen("shared")}
|
||||
>
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Shared With Me
|
||||
</Button>
|
||||
</Button> */}
|
||||
{/* <Button
|
||||
variant="ghost"
|
||||
onClick={() => setScreen("settings")}
|
||||
@ -110,7 +111,7 @@ export default function Dashboard({
|
||||
className="justify-start font-normal text-muted-foreground"
|
||||
>
|
||||
<HelpCircle className="w-4 h-4 mr-2" />
|
||||
About
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -121,7 +122,12 @@ export default function Dashboard({
|
||||
) : null}
|
||||
</>
|
||||
) : screen === "shared" ? (
|
||||
<DashboardSharedWithMe shared={shared} />
|
||||
<DashboardSharedWithMe
|
||||
shared={shared.map((item) => ({
|
||||
...item,
|
||||
authorAvatarUrl: item.authorAvatarUrl || "",
|
||||
}))}
|
||||
/>
|
||||
) : screen === "settings" ? null : null}
|
||||
</div>
|
||||
</>
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { projectTemplates } from "@/lib/data"
|
||||
import { ChevronRight } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
@ -18,7 +19,7 @@ export default function DashboardSharedWithMe({
|
||||
shared: {
|
||||
id: string
|
||||
name: string
|
||||
type: "react" | "node"
|
||||
type: string
|
||||
author: string
|
||||
authorAvatarUrl: string
|
||||
sharedOn: Date
|
||||
@ -46,9 +47,8 @@ export default function DashboardSharedWithMe({
|
||||
<Image
|
||||
alt=""
|
||||
src={
|
||||
sandbox.type === "react"
|
||||
? "/project-icons/react.svg"
|
||||
: "/project-icons/node.svg"
|
||||
projectTemplates.find((p) => p.id === sandbox.type)
|
||||
?.icon ?? "/project-icons/node.svg"
|
||||
}
|
||||
width={20}
|
||||
height={20}
|
||||
@ -59,7 +59,7 @@ export default function DashboardSharedWithMe({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
<Avatar
|
||||
name={sandbox.author}
|
||||
avatarUrl={sandbox.authorAvatarUrl}
|
||||
className="mr-2"
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Send, StopCircle, Image as ImageIcon, Paperclip } from "lucide-react"
|
||||
import { Button } from "../../ui/button"
|
||||
import { useEffect } from "react"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { ALLOWED_FILE_TYPES } from "./types"
|
||||
import { Image as ImageIcon, Paperclip, Send, StopCircle } from "lucide-react"
|
||||
import { useEffect } from "react"
|
||||
import { Button } from "../../ui/button"
|
||||
import { looksLikeCode } from "./lib/chatUtils"
|
||||
import { ChatInputProps } from "./types"
|
||||
import { ALLOWED_FILE_TYPES, ChatInputProps } from "./types"
|
||||
|
||||
export default function ChatInput({
|
||||
input,
|
||||
@ -21,12 +20,11 @@ export default function ChatInput({
|
||||
onRemoveTab,
|
||||
textareaRef,
|
||||
}: ChatInputProps) {
|
||||
|
||||
// Auto-resize textarea as content changes
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
||||
textareaRef.current.style.height = "auto"
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + "px"
|
||||
}
|
||||
}, [input])
|
||||
|
||||
@ -40,7 +38,11 @@ export default function ChatInput({
|
||||
e.preventDefault()
|
||||
handleSend(false)
|
||||
}
|
||||
} else if (e.key === "Backspace" && input === "" && contextTabs.length > 0) {
|
||||
} else if (
|
||||
e.key === "Backspace" &&
|
||||
input === "" &&
|
||||
contextTabs.length > 0
|
||||
) {
|
||||
e.preventDefault()
|
||||
// Remove the last context tab
|
||||
const lastTab = contextTabs[contextTabs.length - 1]
|
||||
@ -51,89 +53,92 @@ export default function ChatInput({
|
||||
// Handle paste events for image and code
|
||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||
// Handle image paste
|
||||
const items = Array.from(e.clipboardData.items);
|
||||
const items = Array.from(e.clipboardData.items)
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
e.preventDefault();
|
||||
|
||||
const file = item.getAsFile();
|
||||
if (!file) continue;
|
||||
if (item.type.startsWith("image/")) {
|
||||
e.preventDefault()
|
||||
|
||||
const file = item.getAsFile()
|
||||
if (!file) continue
|
||||
|
||||
try {
|
||||
// Convert image to base64 string for context tab title and timestamp
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64String = reader.result as string;
|
||||
const base64String = reader.result as string
|
||||
addContextTab(
|
||||
"image",
|
||||
`Image ${new Date().toLocaleTimeString('en-US', {
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).replace(/(\d{2}):(\d{2})/, '$1:$2')}`,
|
||||
`Image ${new Date()
|
||||
.toLocaleTimeString("en-US", {
|
||||
hour12: true,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
.replace(/(\d{2}):(\d{2})/, "$1:$2")}`,
|
||||
base64String
|
||||
);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
} catch (error) {
|
||||
console.error('Error processing pasted image:', error);
|
||||
console.error("Error processing pasted image:", error)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get text from clipboard
|
||||
const text = e.clipboardData.getData('text');
|
||||
|
||||
// Get text from clipboard
|
||||
const text = e.clipboardData.getData("text")
|
||||
|
||||
// If text doesn't contain newlines or doesn't look like code, let it paste normally
|
||||
if (!text || !text.includes('\n') || !looksLikeCode(text)) {
|
||||
return;
|
||||
if (!text || !text.includes("\n") || !looksLikeCode(text)) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const editor = editorRef.current;
|
||||
const currentSelection = editor?.getSelection();
|
||||
const lines = text.split('\n');
|
||||
e.preventDefault()
|
||||
const editor = editorRef.current
|
||||
const currentSelection = editor?.getSelection()
|
||||
const lines = text.split("\n")
|
||||
|
||||
// TODO: FIX THIS: even when i paste the outside code, it shows the active file name,it works when no tabs are open, just does not work when the tab is open
|
||||
|
||||
|
||||
// If selection exists in editor, use file name and line numbers
|
||||
if (currentSelection && !currentSelection.isEmpty()) {
|
||||
addContextTab(
|
||||
"code",
|
||||
`${activeFileName} (${currentSelection.startLineNumber}-${currentSelection.endLineNumber})`,
|
||||
text,
|
||||
{ start: currentSelection.startLineNumber, end: currentSelection.endLineNumber }
|
||||
);
|
||||
return;
|
||||
{
|
||||
start: currentSelection.startLineNumber,
|
||||
end: currentSelection.endLineNumber,
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// If we have stored line range from a copy operation in the editor
|
||||
if (lastCopiedRangeRef.current) {
|
||||
const range = lastCopiedRangeRef.current;
|
||||
const range = lastCopiedRangeRef.current
|
||||
addContextTab(
|
||||
"code",
|
||||
`${activeFileName} (${range.startLine}-${range.endLine})`,
|
||||
text,
|
||||
{ start: range.startLine, end: range.endLine }
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// For code pasted from outside the editor
|
||||
addContextTab(
|
||||
"code",
|
||||
`Pasted Code (1-${lines.length})`,
|
||||
text,
|
||||
{ start: 1, end: lines.length }
|
||||
);
|
||||
};
|
||||
addContextTab("code", `Pasted Code (1-${lines.length})`, text, {
|
||||
start: 1,
|
||||
end: lines.length,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle image upload from local machine via input
|
||||
const handleImageUpload = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = "image/*"
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) onImageUpload(file)
|
||||
@ -155,14 +160,16 @@ export default function ChatInput({
|
||||
|
||||
// Handle file upload from local machine via input
|
||||
const handleFileUpload = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf'
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = ".txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf"
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
if (!(file.type in ALLOWED_FILE_TYPES)) {
|
||||
alert('Unsupported file type. Please upload text, code, or PDF files.')
|
||||
alert(
|
||||
"Unsupported file type. Please upload text, code, or PDF files."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -223,17 +230,16 @@ export default function ChatInput({
|
||||
<span className="hidden sm:inline">File</span>
|
||||
</Button>
|
||||
{/* Render image upload button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 sm:px-3"
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
<ImageIcon className="h-3 w-3 sm:mr-1" />
|
||||
className="h-6 px-2 sm:px-3"
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
<ImageIcon className="h-3 w-3 sm:mr-1" />
|
||||
<span className="hidden sm:inline">Image</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,9 @@ import React, { useState } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import { Button } from "../../ui/button"
|
||||
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||
import ContextTabs from "./ContextTabs"
|
||||
import { createMarkdownComponents } from './lib/markdownComponents'
|
||||
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||
import { createMarkdownComponents } from "./lib/markdownComponents"
|
||||
import { MessageProps } from "./types"
|
||||
|
||||
export default function ChatMessage({
|
||||
@ -14,7 +14,6 @@ export default function ChatMessage({
|
||||
setIsContextExpanded,
|
||||
socket,
|
||||
}: MessageProps) {
|
||||
|
||||
// State for expanded message index
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||
number | null
|
||||
@ -23,7 +22,7 @@ export default function ChatMessage({
|
||||
// State for copied text
|
||||
const [copiedText, setCopiedText] = useState<string | null>(null)
|
||||
|
||||
// Render copy button for text content
|
||||
// Render copy button for text content
|
||||
const renderCopyButton = (text: any) => (
|
||||
<Button
|
||||
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
|
||||
@ -43,26 +42,26 @@ export default function ChatMessage({
|
||||
const askAboutCode = (code: any) => {
|
||||
const contextString = stringifyContent(code)
|
||||
const newContext = `Regarding this code:\n${contextString}`
|
||||
|
||||
|
||||
// Format timestamp to match chat message format (HH:MM PM)
|
||||
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||
const timestamp = new Date().toLocaleTimeString("en-US", {
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
|
||||
|
||||
// Instead of replacing context, append to it
|
||||
if (message.role === "assistant") {
|
||||
// For assistant messages, create a new context tab with the response content and timestamp
|
||||
setContext(newContext, `AI Response (${timestamp})`, {
|
||||
start: 1,
|
||||
end: contextString.split('\n').length
|
||||
end: contextString.split("\n").length,
|
||||
})
|
||||
} else {
|
||||
// For user messages, create a new context tab with the selected content and timestamp
|
||||
setContext(newContext, `User Chat (${timestamp})`, {
|
||||
start: 1,
|
||||
end: contextString.split('\n').length
|
||||
end: contextString.split("\n").length,
|
||||
})
|
||||
}
|
||||
setIsContextExpanded(false)
|
||||
@ -127,7 +126,9 @@ export default function ChatMessage({
|
||||
contextTabs={parseContextToTabs(message.context)}
|
||||
onRemoveTab={() => {}}
|
||||
isExpanded={expandedMessageIndex === 0}
|
||||
onToggleExpand={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
|
||||
onToggleExpand={() =>
|
||||
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
|
||||
}
|
||||
className="[&_div:first-child>div:first-child>div]:bg-[#0D0D0D] [&_button:first-child]:hidden [&_button:last-child]:hidden"
|
||||
/>
|
||||
{expandedMessageIndex === 0 && (
|
||||
@ -153,7 +154,7 @@ export default function ChatMessage({
|
||||
const updatedContext = `Regarding this code:\n${e.target.value}`
|
||||
setContext(updatedContext, "Selected Content", {
|
||||
start: 1,
|
||||
end: e.target.value.split('\n').length
|
||||
end: e.target.value.split("\n").length,
|
||||
})
|
||||
}}
|
||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||
@ -187,10 +188,7 @@ export default function ChatMessage({
|
||||
)}
|
||||
{/* Render markdown content */}
|
||||
{message.role === "assistant" ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={components}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
@ -201,26 +199,28 @@ export default function ChatMessage({
|
||||
)
|
||||
}
|
||||
|
||||
// Parse context to tabs for context tabs component
|
||||
// Parse context to tabs for context tabs component
|
||||
function parseContextToTabs(context: string) {
|
||||
const sections = context.split(/(?=File |Code from )/)
|
||||
return sections.map((section, index) => {
|
||||
const lines = section.trim().split('\n')
|
||||
const titleLine = lines[0]
|
||||
let content = lines.slice(1).join('\n').trim()
|
||||
|
||||
// Remove code block markers for display
|
||||
content = content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '')
|
||||
|
||||
// Determine if the context is a file or code
|
||||
const isFile = titleLine.startsWith('File ')
|
||||
const name = titleLine.replace(/^(File |Code from )/, '').replace(':', '')
|
||||
|
||||
return {
|
||||
id: `context-${index}`,
|
||||
type: isFile ? "file" as const : "code" as const,
|
||||
name: name,
|
||||
content: content
|
||||
}
|
||||
}).filter(tab => tab.content.length > 0)
|
||||
return sections
|
||||
.map((section, index) => {
|
||||
const lines = section.trim().split("\n")
|
||||
const titleLine = lines[0]
|
||||
let content = lines.slice(1).join("\n").trim()
|
||||
|
||||
// Remove code block markers for display
|
||||
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
||||
|
||||
// Determine if the context is a file or code
|
||||
const isFile = titleLine.startsWith("File ")
|
||||
const name = titleLine.replace(/^(File |Code from )/, "").replace(":", "")
|
||||
|
||||
return {
|
||||
id: `context-${index}`,
|
||||
type: isFile ? ("file" as const) : ("code" as const),
|
||||
name: name,
|
||||
content: content,
|
||||
}
|
||||
})
|
||||
.filter((tab) => tab.content.length > 0)
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { Plus, X, Image as ImageIcon, FileText } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "../../ui/button"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ContextTab } from "./types"
|
||||
import { ContextTabsProps } from "./types"
|
||||
// Ignore certain folders and files from the file tree
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { FileText, Image as ImageIcon, Plus, X } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "../../ui/button"
|
||||
import { ContextTab, ContextTabsProps } from "./types"
|
||||
// Ignore certain folders and files from the file tree
|
||||
import { ignoredFiles, ignoredFolders } from "./lib/ignored-paths"
|
||||
|
||||
export default function ContextTabs({
|
||||
@ -20,7 +19,6 @@ export default function ContextTabs({
|
||||
files = [],
|
||||
onFileSelect,
|
||||
}: ContextTabsProps & { className?: string }) {
|
||||
|
||||
// State for preview tab
|
||||
const [previewTab, setPreviewTab] = useState<ContextTab | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
@ -28,9 +26,9 @@ export default function ContextTabs({
|
||||
// Allow preview for images and code selections from editor
|
||||
const togglePreview = (tab: ContextTab) => {
|
||||
if (!tab.lineRange && tab.type !== "image") {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Toggle preview for images and code selections from editor
|
||||
if (previewTab?.id === tab.id) {
|
||||
setPreviewTab(null)
|
||||
@ -50,13 +48,21 @@ export default function ContextTabs({
|
||||
// Get all files from the file tree to search for context
|
||||
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
|
||||
return items.reduce((acc: TFile[], item) => {
|
||||
// Add file if it's not ignored
|
||||
if (item.type === "file" && !ignoredFiles.some((pattern: string) =>
|
||||
item.name.endsWith(pattern.replace('*', '')) || item.name === pattern
|
||||
)) {
|
||||
// Add file if it's not ignored
|
||||
if (
|
||||
item.type === "file" &&
|
||||
!ignoredFiles.some(
|
||||
(pattern: string) =>
|
||||
item.name.endsWith(pattern.replace("*", "")) ||
|
||||
item.name === pattern
|
||||
)
|
||||
) {
|
||||
acc.push(item)
|
||||
// Add all files from folder if it's not ignored
|
||||
} else if (item.type === "folder" && !ignoredFolders.some((folder: string) => folder === item.name)) {
|
||||
// Add all files from folder if it's not ignored
|
||||
} else if (
|
||||
item.type === "folder" &&
|
||||
!ignoredFolders.some((folder: string) => folder === item.name)
|
||||
) {
|
||||
acc.push(...getAllFiles(item.children))
|
||||
}
|
||||
return acc
|
||||
@ -65,22 +71,18 @@ export default function ContextTabs({
|
||||
|
||||
// Get all files from the file tree to search for context when adding context
|
||||
const allFiles = getAllFiles(files)
|
||||
const filteredFiles = allFiles.filter(file =>
|
||||
const filteredFiles = allFiles.filter((file) =>
|
||||
file.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`border-none ${className || ''}`}>
|
||||
<div className={`border-none ${className || ""}`}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1 overflow-hidden mb-2 flex-wrap">
|
||||
{/* Add context tab button */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@ -143,20 +145,23 @@ export default function ContextTabs({
|
||||
{previewTab && (
|
||||
<div className="p-2 bg-input rounded-md max-h-[200px] overflow-auto mb-2">
|
||||
{previewTab.type === "image" ? (
|
||||
<img
|
||||
src={previewTab.content}
|
||||
<img
|
||||
src={previewTab.content}
|
||||
alt={previewTab.name}
|
||||
className="max-w-full h-auto"
|
||||
/>
|
||||
) : previewTab.lineRange && (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Lines {previewTab.lineRange.start}-{previewTab.lineRange.end}
|
||||
</div>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap">
|
||||
{previewTab.content}
|
||||
</pre>
|
||||
</>
|
||||
) : (
|
||||
previewTab.lineRange && (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Lines {previewTab.lineRange.start}-
|
||||
{previewTab.lineRange.end}
|
||||
</div>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap">
|
||||
{previewTab.content}
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{/* Render file context tab */}
|
||||
{previewTab.type === "file" && (
|
||||
@ -169,4 +174,4 @@ export default function ContextTabs({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { X } from "lucide-react"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { TFile } from "@/lib/types"
|
||||
import { X, ChevronDown } from "lucide-react"
|
||||
import { nanoid } from "nanoid"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import LoadingDots from "../../ui/LoadingDots"
|
||||
import ChatInput from "./ChatInput"
|
||||
import ChatMessage from "./ChatMessage"
|
||||
import ContextTabs from "./ContextTabs"
|
||||
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
||||
import { nanoid } from 'nanoid'
|
||||
import { TFile } from "@/lib/types"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { Message, ContextTab, AIChatProps } from './types'
|
||||
import { AIChatProps, ContextTab, Message } from "./types"
|
||||
|
||||
export default function AIChat({
|
||||
activeFileContent,
|
||||
@ -17,6 +17,7 @@ export default function AIChat({
|
||||
editorRef,
|
||||
lastCopiedRangeRef,
|
||||
files,
|
||||
templateType,
|
||||
}: AIChatProps) {
|
||||
// Initialize socket and messages
|
||||
const { socket } = useSocket()
|
||||
@ -38,65 +39,96 @@ export default function AIChat({
|
||||
// Initialize textarea ref
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Scroll to bottom of chat when messages change
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
// state variables for auto scroll and scroll button
|
||||
const [autoScroll, setAutoScroll] = useState(true)
|
||||
const [showScrollButton, setShowScrollButton] = useState(false)
|
||||
|
||||
// Scroll to bottom of chat when messages change
|
||||
const scrollToBottom = () => {
|
||||
if (chatContainerRef.current) {
|
||||
setTimeout(() => {
|
||||
chatContainerRef.current?.scrollTo({
|
||||
top: chatContainerRef.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}, 100)
|
||||
// scroll to bottom of chat when messages change
|
||||
useEffect(() => {
|
||||
if (autoScroll) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}, [messages, autoScroll])
|
||||
|
||||
// scroll to bottom of chat when messages change
|
||||
const scrollToBottom = (force: boolean = false) => {
|
||||
if (!chatContainerRef.current || (!autoScroll && !force)) return
|
||||
|
||||
chatContainerRef.current.scrollTo({
|
||||
top: chatContainerRef.current.scrollHeight,
|
||||
behavior: force ? "smooth" : "auto",
|
||||
})
|
||||
}
|
||||
|
||||
// function to handle scroll events
|
||||
const handleScroll = () => {
|
||||
if (!chatContainerRef.current) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current
|
||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 50
|
||||
|
||||
setAutoScroll(isAtBottom)
|
||||
setShowScrollButton(!isAtBottom)
|
||||
}
|
||||
|
||||
// scroll event listener
|
||||
useEffect(() => {
|
||||
const container = chatContainerRef.current
|
||||
if (container) {
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
return () => container.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Add context tab to context tabs
|
||||
const addContextTab = (type: string, name: string, content: string, lineRange?: { start: number; end: number }) => {
|
||||
const addContextTab = (
|
||||
type: string,
|
||||
name: string,
|
||||
content: string,
|
||||
lineRange?: { start: number; end: number }
|
||||
) => {
|
||||
const newTab = {
|
||||
id: nanoid(),
|
||||
type: type as "file" | "code" | "image",
|
||||
name,
|
||||
content,
|
||||
lineRange
|
||||
lineRange,
|
||||
}
|
||||
setContextTabs(prev => [...prev, newTab])
|
||||
setContextTabs((prev) => [...prev, newTab])
|
||||
}
|
||||
|
||||
// Remove context tab from context tabs
|
||||
const removeContextTab = (id: string) => {
|
||||
setContextTabs(prev => prev.filter(tab => tab.id !== id))
|
||||
setContextTabs((prev) => prev.filter((tab) => tab.id !== id))
|
||||
}
|
||||
|
||||
// Add file to context tabs
|
||||
const handleAddFile = (tab: ContextTab) => {
|
||||
setContextTabs(prev => [...prev, tab])
|
||||
setContextTabs((prev) => [...prev, tab])
|
||||
}
|
||||
|
||||
// Format code content to remove starting and ending code block markers if they exist
|
||||
const formatCodeContent = (content: string) => {
|
||||
return content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '')
|
||||
return content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
||||
}
|
||||
|
||||
// Get combined context from context tabs
|
||||
const getCombinedContext = () => {
|
||||
if (contextTabs.length === 0) return ''
|
||||
|
||||
return contextTabs.map(tab => {
|
||||
if (tab.type === 'file') {
|
||||
const fileExt = tab.name.split('.').pop() || 'txt'
|
||||
const cleanContent = formatCodeContent(tab.content)
|
||||
return `File ${tab.name}:\n\`\`\`${fileExt}\n${cleanContent}\n\`\`\``
|
||||
} else if (tab.type === 'code') {
|
||||
const cleanContent = formatCodeContent(tab.content)
|
||||
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
||||
}
|
||||
return `${tab.name}:\n${tab.content}`
|
||||
}).join('\n\n')
|
||||
if (contextTabs.length === 0) return ""
|
||||
|
||||
return contextTabs
|
||||
.map((tab) => {
|
||||
if (tab.type === "file") {
|
||||
const fileExt = tab.name.split(".").pop() || "txt"
|
||||
const cleanContent = formatCodeContent(tab.content)
|
||||
return `File ${tab.name}:\n\`\`\`${fileExt}\n${cleanContent}\n\`\`\``
|
||||
} else if (tab.type === "code") {
|
||||
const cleanContent = formatCodeContent(tab.content)
|
||||
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
||||
}
|
||||
return `${tab.name}:\n${tab.content}`
|
||||
})
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
// Handle sending message with context
|
||||
@ -112,7 +144,9 @@ export default function AIChat({
|
||||
setIsGenerating,
|
||||
setIsLoading,
|
||||
abortControllerRef,
|
||||
activeFileContent
|
||||
activeFileContent,
|
||||
false,
|
||||
templateType
|
||||
)
|
||||
// Clear context tabs after sending
|
||||
setContextTabs([])
|
||||
@ -120,9 +154,9 @@ export default function AIChat({
|
||||
|
||||
// Set context for the chat
|
||||
const setContext = (
|
||||
context: string | null,
|
||||
name: string,
|
||||
range?: { start: number, end: number }
|
||||
context: string | null,
|
||||
name: string,
|
||||
range?: { start: number; end: number }
|
||||
) => {
|
||||
if (!context) {
|
||||
setContextTabs([])
|
||||
@ -130,7 +164,7 @@ export default function AIChat({
|
||||
}
|
||||
|
||||
// Always add a new tab instead of updating existing ones
|
||||
addContextTab('code', name, context, range)
|
||||
addContextTab("code", name, context, range)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -153,10 +187,10 @@ export default function AIChat({
|
||||
</div>
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className="flex-grow overflow-y-auto p-4 space-y-4"
|
||||
className="flex-grow overflow-y-auto p-4 space-y-4 relative"
|
||||
>
|
||||
{messages.map((message, messageIndex) => (
|
||||
// Render chat message component for each message
|
||||
// Render chat message component for each message
|
||||
<ChatMessage
|
||||
key={messageIndex}
|
||||
message={message}
|
||||
@ -166,6 +200,17 @@ export default function AIChat({
|
||||
/>
|
||||
))}
|
||||
{isLoading && <LoadingDots />}
|
||||
|
||||
{/* Add scroll to bottom button */}
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={() => scrollToBottom(true)}
|
||||
className="fixed bottom-36 right-6 bg-primary text-primary-foreground rounded-md border border-primary p-0.5 shadow-lg hover:bg-primary/90 transition-all"
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 border-t mb-14">
|
||||
{/* Render context tabs component */}
|
||||
@ -180,9 +225,9 @@ export default function AIChat({
|
||||
socket={socket}
|
||||
onFileSelect={(file: TFile) => {
|
||||
socket?.emit("getFile", { fileId: file.id }, (response: string) => {
|
||||
const fileExt = file.name.split('.').pop() || 'txt'
|
||||
const fileExt = file.name.split(".").pop() || "txt"
|
||||
const formattedContent = `\`\`\`${fileExt}\n${response}\n\`\`\``
|
||||
addContextTab('file', file.name, formattedContent)
|
||||
addContextTab("file", file.name, formattedContent)
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
}
|
||||
@ -210,9 +255,9 @@ export default function AIChat({
|
||||
}}
|
||||
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||
activeFileName={activeFileName}
|
||||
contextTabs={contextTabs.map(tab => ({
|
||||
contextTabs={contextTabs.map((tab) => ({
|
||||
...tab,
|
||||
title: tab.id
|
||||
title: tab.id,
|
||||
}))}
|
||||
onRemoveTab={removeContextTab}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
|
||||
// Stringify content for chat message component
|
||||
// Stringify content for chat message component
|
||||
export const stringifyContent = (
|
||||
content: any,
|
||||
seen = new WeakSet()
|
||||
@ -66,19 +66,19 @@ export const stringifyContent = (
|
||||
return String(content)
|
||||
}
|
||||
|
||||
// Copy to clipboard for chat message component
|
||||
// Copy to clipboard for chat message component
|
||||
export const copyToClipboard = (
|
||||
text: string,
|
||||
setCopiedText: (text: string | null) => void
|
||||
) => {
|
||||
// Copy text to clipboard for chat message component
|
||||
// Copy text to clipboard for chat message component
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedText(text)
|
||||
setTimeout(() => setCopiedText(null), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle send for chat message component
|
||||
// Handle send for chat message component
|
||||
export const handleSend = async (
|
||||
input: string,
|
||||
context: string | null,
|
||||
@ -89,27 +89,31 @@ 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,
|
||||
templateType: string
|
||||
) => {
|
||||
// Return if input is empty and context is null
|
||||
if (input.trim() === "" && !context) return
|
||||
if (input.trim() === "" && !context) return
|
||||
|
||||
// Get timestamp for chat message component
|
||||
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).replace(/(\d{2}):(\d{2})/, '$1:$2')
|
||||
// Get timestamp for chat message component
|
||||
const timestamp = new Date()
|
||||
.toLocaleTimeString("en-US", {
|
||||
hour12: true,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
.replace(/(\d{2}):(\d{2})/, "$1:$2")
|
||||
|
||||
// Create user message for chat message component
|
||||
// Create user message for chat message component
|
||||
const userMessage = {
|
||||
role: "user" as const,
|
||||
content: input,
|
||||
context: context || undefined,
|
||||
timestamp: timestamp
|
||||
timestamp: timestamp,
|
||||
}
|
||||
|
||||
// Update messages for chat message component
|
||||
// Update messages for chat message component
|
||||
const updatedMessages = [...messages, userMessage]
|
||||
setMessages(updatedMessages)
|
||||
setInput("")
|
||||
@ -120,24 +124,25 @@ export const handleSend = async (
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
try {
|
||||
// Create anthropic messages for chat message component
|
||||
// Create anthropic messages for chat message component
|
||||
const anthropicMessages = updatedMessages.map((msg) => ({
|
||||
role: msg.role === "user" ? "human" : "assistant",
|
||||
content: msg.content,
|
||||
}))
|
||||
|
||||
// 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",
|
||||
// Fetch AI response for chat message component
|
||||
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,
|
||||
templateType: templateType,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
}
|
||||
@ -145,22 +150,23 @@ 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
|
||||
// Get reader for chat message component
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const assistantMessage = { role: "assistant" as const, content: "" }
|
||||
setMessages([...updatedMessages, assistantMessage])
|
||||
setIsLoading(false)
|
||||
|
||||
// Initialize buffer for chat message component
|
||||
// Initialize buffer for chat message component
|
||||
let buffer = ""
|
||||
const updateInterval = 100
|
||||
let lastUpdateTime = Date.now()
|
||||
|
||||
// Read response from reader for chat message component
|
||||
// Read response from reader for chat message component
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
@ -179,7 +185,7 @@ export const handleSend = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Update messages for chat message component
|
||||
// Update messages for chat message component
|
||||
setMessages((prev) => {
|
||||
const updatedMessages = [...prev]
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||
@ -188,14 +194,14 @@ export const handleSend = async (
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Handle abort error for chat message component
|
||||
// Handle abort error for chat message component
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Generation aborted")
|
||||
} else {
|
||||
console.error("Error fetching AI response:", error)
|
||||
const errorMessage = {
|
||||
role: "assistant" as const,
|
||||
content: "Sorry, I encountered an error. Please try again.",
|
||||
content: error.message || "Sorry, I encountered an error. Please try again.",
|
||||
}
|
||||
setMessages((prev) => [...prev, errorMessage])
|
||||
}
|
||||
@ -206,7 +212,7 @@ export const handleSend = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stop generation for chat message component
|
||||
// Handle stop generation for chat message component
|
||||
export const handleStopGeneration = (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>
|
||||
) => {
|
||||
@ -215,21 +221,21 @@ export const handleStopGeneration = (
|
||||
}
|
||||
}
|
||||
|
||||
// Check if text looks like code for chat message component
|
||||
// Check if text looks like code for chat message component
|
||||
export const looksLikeCode = (text: string): boolean => {
|
||||
const codeIndicators = [
|
||||
/^import\s+/m, // import statements
|
||||
/^function\s+/m, // function declarations
|
||||
/^class\s+/m, // class declarations
|
||||
/^const\s+/m, // const declarations
|
||||
/^let\s+/m, // let declarations
|
||||
/^var\s+/m, // var declarations
|
||||
/[{}\[\]();]/, // common code syntax
|
||||
/^\s*\/\//m, // comments
|
||||
/^\s*\/\*/m, // multi-line comments
|
||||
/=>/, // arrow functions
|
||||
/^export\s+/m, // export statements
|
||||
];
|
||||
/^import\s+/m, // import statements
|
||||
/^function\s+/m, // function declarations
|
||||
/^class\s+/m, // class declarations
|
||||
/^const\s+/m, // const declarations
|
||||
/^let\s+/m, // let declarations
|
||||
/^var\s+/m, // var declarations
|
||||
/[{}\[\]();]/, // common code syntax
|
||||
/^\s*\/\//m, // comments
|
||||
/^\s*\/\*/m, // multi-line comments
|
||||
/=>/, // arrow functions
|
||||
/^export\s+/m, // export statements
|
||||
]
|
||||
|
||||
return codeIndicators.some(pattern => pattern.test(text));
|
||||
};
|
||||
return codeIndicators.some((pattern) => pattern.test(text))
|
||||
}
|
||||
|
@ -1,102 +1,102 @@
|
||||
// Ignore certain folders and files from the file tree
|
||||
// Ignore certain folders and files from the file tree
|
||||
|
||||
export const ignoredFolders = [
|
||||
// Package managers
|
||||
'node_modules',
|
||||
'venv',
|
||||
'.env',
|
||||
'env',
|
||||
'.venv',
|
||||
'virtualenv',
|
||||
'pip-wheel-metadata',
|
||||
|
||||
// Build outputs
|
||||
'.next',
|
||||
'dist',
|
||||
'build',
|
||||
'out',
|
||||
'__pycache__',
|
||||
'.webpack',
|
||||
'.serverless',
|
||||
'storybook-static',
|
||||
|
||||
// Version control
|
||||
'.git',
|
||||
'.svn',
|
||||
'.hg', // Mercurial
|
||||
|
||||
// Cache and temp files
|
||||
'.cache',
|
||||
'coverage',
|
||||
'tmp',
|
||||
'.temp',
|
||||
'.npm',
|
||||
'.pnpm',
|
||||
'.yarn',
|
||||
'.eslintcache',
|
||||
'.stylelintcache',
|
||||
|
||||
// IDE specific
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'.vs',
|
||||
'.sublime',
|
||||
|
||||
// Framework specific
|
||||
'.streamlit',
|
||||
'.next',
|
||||
'static',
|
||||
'.pytest_cache',
|
||||
'.nuxt',
|
||||
'.docusaurus',
|
||||
'.remix',
|
||||
'.parcel-cache',
|
||||
'public/build', // Remix/Rails
|
||||
'.turbo', // Turborepo
|
||||
|
||||
// Logs
|
||||
'logs',
|
||||
'*.log',
|
||||
'npm-debug.log*',
|
||||
'yarn-debug.log*',
|
||||
'yarn-error.log*',
|
||||
'pnpm-debug.log*',
|
||||
] as const;
|
||||
|
||||
export const ignoredFiles = [
|
||||
'.DS_Store',
|
||||
'.env.local',
|
||||
'.env.development',
|
||||
'.env.production',
|
||||
'.env.test',
|
||||
'.env*.local',
|
||||
'.gitignore',
|
||||
'.npmrc',
|
||||
'.yarnrc',
|
||||
'.editorconfig',
|
||||
'.prettierrc',
|
||||
'.eslintrc',
|
||||
'.browserslistrc',
|
||||
'tsconfig.tsbuildinfo',
|
||||
'*.pyc',
|
||||
'*.pyo',
|
||||
'*.pyd',
|
||||
'*.so',
|
||||
'*.dll',
|
||||
'*.dylib',
|
||||
'*.class',
|
||||
'*.exe',
|
||||
'package-lock.json',
|
||||
'yarn.lock',
|
||||
'pnpm-lock.yaml',
|
||||
'composer.lock',
|
||||
'poetry.lock',
|
||||
'Gemfile.lock',
|
||||
'*.min.js',
|
||||
'*.min.css',
|
||||
'*.map',
|
||||
'*.chunk.*',
|
||||
'*.hot-update.*',
|
||||
'.vercel',
|
||||
'.netlify'
|
||||
] as const;
|
||||
// Package managers
|
||||
"node_modules",
|
||||
"venv",
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"virtualenv",
|
||||
"pip-wheel-metadata",
|
||||
|
||||
// Build outputs
|
||||
".next",
|
||||
"dist",
|
||||
"build",
|
||||
"out",
|
||||
"__pycache__",
|
||||
".webpack",
|
||||
".serverless",
|
||||
"storybook-static",
|
||||
|
||||
// Version control
|
||||
".git",
|
||||
".svn",
|
||||
".hg", // Mercurial
|
||||
|
||||
// Cache and temp files
|
||||
".cache",
|
||||
"coverage",
|
||||
"tmp",
|
||||
".temp",
|
||||
".npm",
|
||||
".pnpm",
|
||||
".yarn",
|
||||
".eslintcache",
|
||||
".stylelintcache",
|
||||
|
||||
// IDE specific
|
||||
".idea",
|
||||
".vscode",
|
||||
".vs",
|
||||
".sublime",
|
||||
|
||||
// Framework specific
|
||||
".streamlit",
|
||||
".next",
|
||||
"static",
|
||||
".pytest_cache",
|
||||
".nuxt",
|
||||
".docusaurus",
|
||||
".remix",
|
||||
".parcel-cache",
|
||||
"public/build", // Remix/Rails
|
||||
".turbo", // Turborepo
|
||||
|
||||
// Logs
|
||||
"logs",
|
||||
"*.log",
|
||||
"npm-debug.log*",
|
||||
"yarn-debug.log*",
|
||||
"yarn-error.log*",
|
||||
"pnpm-debug.log*",
|
||||
] as const
|
||||
|
||||
export const ignoredFiles = [
|
||||
".DS_Store",
|
||||
".env.local",
|
||||
".env.development",
|
||||
".env.production",
|
||||
".env.test",
|
||||
".env*.local",
|
||||
".gitignore",
|
||||
".npmrc",
|
||||
".yarnrc",
|
||||
".editorconfig",
|
||||
".prettierrc",
|
||||
".eslintrc",
|
||||
".browserslistrc",
|
||||
"tsconfig.tsbuildinfo",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.pyd",
|
||||
"*.so",
|
||||
"*.dll",
|
||||
"*.dylib",
|
||||
"*.class",
|
||||
"*.exe",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"composer.lock",
|
||||
"poetry.lock",
|
||||
"Gemfile.lock",
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
"*.map",
|
||||
"*.chunk.*",
|
||||
"*.hot-update.*",
|
||||
".vercel",
|
||||
".netlify",
|
||||
] as const
|
||||
|
@ -1,21 +1,26 @@
|
||||
import { CornerUpLeft } from "lucide-react"
|
||||
import { Components } from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import { Button } from "../../../ui/button"
|
||||
import { CornerUpLeft } from "lucide-react"
|
||||
import { stringifyContent } from "./chatUtils"
|
||||
|
||||
// Create markdown components for chat message component
|
||||
// Create markdown components for chat message component
|
||||
export const createMarkdownComponents = (
|
||||
renderCopyButton: (text: any) => JSX.Element,
|
||||
renderMarkdownElement: (props: any) => JSX.Element,
|
||||
askAboutCode: (code: any) => void
|
||||
): Components => ({
|
||||
code: ({ node, className, children, ...props }: {
|
||||
node?: import('hast').Element,
|
||||
className?: string,
|
||||
children?: React.ReactNode,
|
||||
[key: string]: any,
|
||||
code: ({
|
||||
node,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
node?: import("hast").Element
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
[key: string]: any
|
||||
}) => {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
|
||||
@ -55,25 +60,30 @@ export const createMarkdownComponents = (
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>{children}</code>
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
// Render markdown elements
|
||||
p: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h1: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h2: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h3: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h4: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h5: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h6: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
// Render markdown elements
|
||||
p: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h1: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h2: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h3: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h4: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h5: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h6: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
ul: (props) => (
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ul>
|
||||
<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>
|
||||
<ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>
|
||||
),
|
||||
})
|
||||
|
@ -1,28 +1,29 @@
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { TemplateConfig } from "@/lib/templates"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { Socket } from 'socket.io-client';
|
||||
import * as monaco from "monaco-editor"
|
||||
import { Socket } from "socket.io-client"
|
||||
|
||||
// Allowed file types for context tabs
|
||||
export const ALLOWED_FILE_TYPES = {
|
||||
// Text files
|
||||
'text/plain': true,
|
||||
'text/markdown': true,
|
||||
'text/csv': true,
|
||||
"text/plain": true,
|
||||
"text/markdown": true,
|
||||
"text/csv": true,
|
||||
// Code files
|
||||
'application/json': true,
|
||||
'text/javascript': true,
|
||||
'text/typescript': true,
|
||||
'text/html': true,
|
||||
'text/css': true,
|
||||
"application/json": true,
|
||||
"text/javascript": true,
|
||||
"text/typescript": true,
|
||||
"text/html": true,
|
||||
"text/css": true,
|
||||
// Documents
|
||||
'application/pdf': true,
|
||||
"application/pdf": true,
|
||||
// Images
|
||||
'image/jpeg': true,
|
||||
'image/png': true,
|
||||
'image/gif': true,
|
||||
'image/webp': true,
|
||||
'image/svg+xml': true,
|
||||
} as const;
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
"image/svg+xml": true,
|
||||
} as const
|
||||
|
||||
// Message interface
|
||||
export interface Message {
|
||||
@ -45,9 +46,16 @@ export interface AIChatProps {
|
||||
activeFileContent: string
|
||||
activeFileName: string
|
||||
onClose: () => void
|
||||
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||
editorRef: React.MutableRefObject<
|
||||
monaco.editor.IStandaloneCodeEditor | undefined
|
||||
>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{
|
||||
startLine: number
|
||||
endLine: number
|
||||
} | null>
|
||||
files: (TFile | TFolder)[]
|
||||
templateType: string
|
||||
templateConfig?: TemplateConfig
|
||||
}
|
||||
|
||||
// Chat input props interface
|
||||
@ -58,11 +66,27 @@ export interface ChatInputProps {
|
||||
handleSend: (useFullContext?: boolean) => void
|
||||
handleStopGeneration: () => void
|
||||
onImageUpload: (file: File) => void
|
||||
addContextTab: (type: string, title: string, content: string, lineRange?: { start: number, end: number }) => void
|
||||
addContextTab: (
|
||||
type: string,
|
||||
title: string,
|
||||
content: string,
|
||||
lineRange?: { start: number; end: number }
|
||||
) => void
|
||||
activeFileName?: string
|
||||
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||
contextTabs: { id: string; type: string; title: string; content: string; lineRange?: { start: number; end: number } }[]
|
||||
editorRef: React.MutableRefObject<
|
||||
monaco.editor.IStandaloneCodeEditor | undefined
|
||||
>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{
|
||||
startLine: number
|
||||
endLine: number
|
||||
} | null>
|
||||
contextTabs: {
|
||||
id: string
|
||||
type: string
|
||||
title: string
|
||||
content: string
|
||||
lineRange?: { start: number; end: number }
|
||||
}[]
|
||||
onRemoveTab: (id: string) => void
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>
|
||||
}
|
||||
@ -74,7 +98,11 @@ export interface MessageProps {
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
setContext: (context: string | null, name: string, range?: { start: number, end: number }) => void
|
||||
setContext: (
|
||||
context: string | null,
|
||||
name: string,
|
||||
range?: { start: number; end: number }
|
||||
) => void
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
socket: Socket | null
|
||||
}
|
||||
|
@ -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>) => {
|
||||
|
@ -7,11 +7,11 @@ import * as monaco from "monaco-editor"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
|
||||
import LiveblocksProvider from "@liveblocks/yjs"
|
||||
import { MonacoBinding } from "y-monaco"
|
||||
import { Awareness } from "y-protocols/awareness"
|
||||
import * as Y from "yjs"
|
||||
// import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
|
||||
// import LiveblocksProvider from "@liveblocks/yjs"
|
||||
// import { MonacoBinding } from "y-monaco"
|
||||
// import { Awareness } from "y-protocols/awareness"
|
||||
// import * as Y from "yjs"
|
||||
|
||||
import {
|
||||
ResizableHandle,
|
||||
@ -23,7 +23,6 @@ import { useSocket } from "@/context/SocketContext"
|
||||
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
|
||||
import { Sandbox, TFile, TFolder, TTab, User } from "@/lib/types"
|
||||
import {
|
||||
addNew,
|
||||
cn,
|
||||
debounce,
|
||||
deepMerge,
|
||||
@ -46,7 +45,7 @@ import { Button } from "../ui/button"
|
||||
import Tab from "../ui/tab"
|
||||
import AIChat from "./AIChat"
|
||||
import GenerateInput from "./generate"
|
||||
import { Cursors } from "./live/cursors"
|
||||
// import { Cursors } from "./live/cursors"
|
||||
import DisableAccessModal from "./live/disableModal"
|
||||
import Loading from "./loading"
|
||||
import PreviewWindow from "./preview"
|
||||
@ -147,20 +146,20 @@ export default function CodeEditor({
|
||||
const isOwner = sandboxData.userId === userData.id
|
||||
const clerk = useClerk()
|
||||
|
||||
// Liveblocks hooks
|
||||
const room = useRoom()
|
||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
||||
const userInfo = useSelf((me) => me.info)
|
||||
// // Liveblocks hooks
|
||||
// const room = useRoom()
|
||||
// const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
||||
// const userInfo = useSelf((me) => me.info)
|
||||
|
||||
// Liveblocks providers map to prevent reinitializing providers
|
||||
type ProviderData = {
|
||||
provider: LiveblocksProvider<never, never, never, never>
|
||||
yDoc: Y.Doc
|
||||
yText: Y.Text
|
||||
binding?: MonacoBinding
|
||||
onSync: (isSynced: boolean) => void
|
||||
}
|
||||
const providersMap = useRef(new Map<string, ProviderData>())
|
||||
// // Liveblocks providers map to prevent reinitializing providers
|
||||
// type ProviderData = {
|
||||
// provider: LiveblocksProvider<never, never, never, never>
|
||||
// yDoc: Y.Doc
|
||||
// yText: Y.Text
|
||||
// binding?: MonacoBinding
|
||||
// onSync: (isSynced: boolean) => void
|
||||
// }
|
||||
// const providersMap = useRef(new Map<string, ProviderData>())
|
||||
|
||||
// Refs for libraries / features
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||
@ -173,7 +172,10 @@ export default function CodeEditor({
|
||||
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
||||
|
||||
// Ref to store the last copied range in the editor to be used in the AIChat component
|
||||
const lastCopiedRangeRef = useRef<{ startLine: number; endLine: number } | null>(null);
|
||||
const lastCopiedRangeRef = useRef<{
|
||||
startLine: number
|
||||
endLine: number
|
||||
} | null>(null)
|
||||
|
||||
const debouncedSetIsSelected = useRef(
|
||||
debounce((value: boolean) => {
|
||||
@ -260,14 +262,14 @@ export default function CodeEditor({
|
||||
|
||||
// Store the last copied range in the editor to be used in the AIChat component
|
||||
editor.onDidChangeCursorSelection((e) => {
|
||||
const selection = editor.getSelection();
|
||||
const selection = editor.getSelection()
|
||||
if (selection) {
|
||||
lastCopiedRangeRef.current = {
|
||||
startLine: selection.startLineNumber,
|
||||
endLine: selection.endLineNumber
|
||||
};
|
||||
endLine: selection.endLineNumber,
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Call the function with your file structure
|
||||
@ -571,82 +573,82 @@ export default function CodeEditor({
|
||||
}
|
||||
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
|
||||
|
||||
// Liveblocks live collaboration setup effect
|
||||
useEffect(() => {
|
||||
const tab = tabs.find((t) => t.id === activeFileId)
|
||||
const model = editorRef?.getModel()
|
||||
// // Liveblocks live collaboration setup effect
|
||||
// useEffect(() => {
|
||||
// const tab = tabs.find((t) => t.id === activeFileId)
|
||||
// const model = editorRef?.getModel()
|
||||
|
||||
if (!editorRef || !tab || !model) return
|
||||
// if (!editorRef || !tab || !model) return
|
||||
|
||||
let providerData: ProviderData
|
||||
// let providerData: ProviderData
|
||||
|
||||
// When a file is opened for the first time, create a new provider and store in providersMap.
|
||||
if (!providersMap.current.has(tab.id)) {
|
||||
const yDoc = new Y.Doc()
|
||||
const yText = yDoc.getText(tab.id)
|
||||
const yProvider = new LiveblocksProvider(room, yDoc)
|
||||
// // When a file is opened for the first time, create a new provider and store in providersMap.
|
||||
// if (!providersMap.current.has(tab.id)) {
|
||||
// const yDoc = new Y.Doc()
|
||||
// const yText = yDoc.getText(tab.id)
|
||||
// const yProvider = new LiveblocksProvider(room, yDoc)
|
||||
|
||||
// Inserts the file content into the editor once when the tab is changed.
|
||||
const onSync = (isSynced: boolean) => {
|
||||
if (isSynced) {
|
||||
const text = yText.toString()
|
||||
if (text === "") {
|
||||
if (activeFileContent) {
|
||||
yText.insert(0, activeFileContent)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
yText.insert(0, editorRef.getValue())
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// // Inserts the file content into the editor once when the tab is changed.
|
||||
// const onSync = (isSynced: boolean) => {
|
||||
// if (isSynced) {
|
||||
// const text = yText.toString()
|
||||
// if (text === "") {
|
||||
// if (activeFileContent) {
|
||||
// yText.insert(0, activeFileContent)
|
||||
// } else {
|
||||
// setTimeout(() => {
|
||||
// yText.insert(0, editorRef.getValue())
|
||||
// }, 0)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
yProvider.on("sync", onSync)
|
||||
// yProvider.on("sync", onSync)
|
||||
|
||||
// Save the provider to the map.
|
||||
providerData = { provider: yProvider, yDoc, yText, onSync }
|
||||
providersMap.current.set(tab.id, providerData)
|
||||
} else {
|
||||
// When a tab is opened that has been open before, reuse the existing provider.
|
||||
providerData = providersMap.current.get(tab.id)!
|
||||
}
|
||||
// // Save the provider to the map.
|
||||
// providerData = { provider: yProvider, yDoc, yText, onSync }
|
||||
// providersMap.current.set(tab.id, providerData)
|
||||
// } else {
|
||||
// // When a tab is opened that has been open before, reuse the existing provider.
|
||||
// providerData = providersMap.current.get(tab.id)!
|
||||
// }
|
||||
|
||||
const binding = new MonacoBinding(
|
||||
providerData.yText,
|
||||
model,
|
||||
new Set([editorRef]),
|
||||
providerData.provider.awareness as unknown as Awareness
|
||||
)
|
||||
// const binding = new MonacoBinding(
|
||||
// providerData.yText,
|
||||
// model,
|
||||
// new Set([editorRef]),
|
||||
// providerData.provider.awareness as unknown as Awareness
|
||||
// )
|
||||
|
||||
providerData.binding = binding
|
||||
setProvider(providerData.provider)
|
||||
// providerData.binding = binding
|
||||
// setProvider(providerData.provider)
|
||||
|
||||
return () => {
|
||||
// Cleanup logic
|
||||
if (binding) {
|
||||
binding.destroy()
|
||||
}
|
||||
if (providerData.binding) {
|
||||
providerData.binding = undefined
|
||||
}
|
||||
}
|
||||
}, [room, activeFileContent])
|
||||
// return () => {
|
||||
// // Cleanup logic
|
||||
// if (binding) {
|
||||
// binding.destroy()
|
||||
// }
|
||||
// if (providerData.binding) {
|
||||
// providerData.binding = undefined
|
||||
// }
|
||||
// }
|
||||
// }, [room, activeFileContent])
|
||||
|
||||
// Added this effect to clean up when the component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clean up all providers when the component unmounts
|
||||
providersMap.current.forEach((data) => {
|
||||
if (data.binding) {
|
||||
data.binding.destroy()
|
||||
}
|
||||
data.provider.disconnect()
|
||||
data.yDoc.destroy()
|
||||
})
|
||||
providersMap.current.clear()
|
||||
}
|
||||
}, [])
|
||||
// // Added this effect to clean up when the component unmounts
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// // Clean up all providers when the component unmounts
|
||||
// providersMap.current.forEach((data) => {
|
||||
// if (data.binding) {
|
||||
// data.binding.destroy()
|
||||
// }
|
||||
// data.provider.disconnect()
|
||||
// data.yDoc.destroy()
|
||||
// })
|
||||
// providersMap.current.clear()
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
// Connection/disconnection effect
|
||||
useEffect(() => {
|
||||
@ -658,7 +660,7 @@ export default function CodeEditor({
|
||||
|
||||
// Socket event listener effect
|
||||
useEffect(() => {
|
||||
const onConnect = () => { }
|
||||
const onConnect = () => {}
|
||||
|
||||
const onDisconnect = () => {
|
||||
setTerminals([])
|
||||
@ -786,8 +788,8 @@ export default function CodeEditor({
|
||||
? numTabs === 1
|
||||
? null
|
||||
: index < numTabs - 1
|
||||
? tabs[index + 1].id
|
||||
: tabs[index - 1].id
|
||||
? tabs[index + 1].id
|
||||
: tabs[index - 1].id
|
||||
: activeFileId
|
||||
|
||||
setTabs((prev) => prev.filter((t) => t.id !== id))
|
||||
@ -853,9 +855,7 @@ export default function CodeEditor({
|
||||
}
|
||||
|
||||
const handleDeleteFile = (file: TFile) => {
|
||||
socket?.emit("deleteFile", { fileId: file.id }, (response: (TFolder | TFile)[]) => {
|
||||
setFiles(response)
|
||||
})
|
||||
socket?.emit("deleteFile", { fileId: file.id })
|
||||
closeTab(file.id)
|
||||
}
|
||||
|
||||
@ -867,10 +867,13 @@ export default function CodeEditor({
|
||||
closeTabs(response)
|
||||
)
|
||||
|
||||
socket?.emit("deleteFolder", { folderId: folder.id }, (response: (TFolder | TFile)[]) => {
|
||||
setFiles(response)
|
||||
setDeletingFolderId("")
|
||||
})
|
||||
socket?.emit(
|
||||
"deleteFolder",
|
||||
{ folderId: folder.id },
|
||||
(response: (TFolder | TFile)[]) => {
|
||||
setDeletingFolderId("")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const togglePreviewPanel = () => {
|
||||
@ -911,7 +914,7 @@ export default function CodeEditor({
|
||||
<DisableAccessModal
|
||||
message={disableAccess.message}
|
||||
open={disableAccess.isDisabled}
|
||||
setOpen={() => { }}
|
||||
setOpen={() => {}}
|
||||
/>
|
||||
<Loading />
|
||||
</>
|
||||
@ -946,15 +949,14 @@ export default function CodeEditor({
|
||||
{generate.show ? (
|
||||
<GenerateInput
|
||||
user={userData}
|
||||
socket={socket!}
|
||||
width={generate.width - 90}
|
||||
data={{
|
||||
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||
code:
|
||||
(isSelected && editorRef?.getSelection()
|
||||
? editorRef
|
||||
?.getModel()
|
||||
?.getValueInRange(editorRef?.getSelection()!)
|
||||
?.getModel()
|
||||
?.getValueInRange(editorRef?.getSelection()!)
|
||||
: editorRef?.getValue()) ?? "",
|
||||
line: generate.line,
|
||||
}}
|
||||
@ -1036,7 +1038,6 @@ export default function CodeEditor({
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
socket={socket!}
|
||||
setFiles={setFiles}
|
||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||
deletingFolderId={deletingFolderId}
|
||||
toggleAIChat={toggleAIChat}
|
||||
isAIChatOpen={isAIChatOpen}
|
||||
@ -1088,9 +1089,9 @@ export default function CodeEditor({
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{provider && userInfo ? (
|
||||
{/* {provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null}
|
||||
) : null} */}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
@ -1151,10 +1152,10 @@ export default function CodeEditor({
|
||||
isAIChatOpen && isHorizontalLayout
|
||||
? "horizontal"
|
||||
: isAIChatOpen
|
||||
? "vertical"
|
||||
: isHorizontalLayout
|
||||
? "horizontal"
|
||||
: "vertical"
|
||||
? "vertical"
|
||||
: isHorizontalLayout
|
||||
? "horizontal"
|
||||
: "vertical"
|
||||
}
|
||||
>
|
||||
<ResizablePanel
|
||||
@ -1232,6 +1233,7 @@ export default function CodeEditor({
|
||||
editorRef={{ current: editorRef }}
|
||||
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||
files={files}
|
||||
templateType={sandboxData.type}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
|
@ -1,29 +1,36 @@
|
||||
import JSZip from 'jszip'
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
// React component for download button
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { Download } from "lucide-react"
|
||||
|
||||
export default function DownloadButton({ name }: { name: string }) {
|
||||
const { socket } = useSocket()
|
||||
|
||||
const handleDownload = async () => {
|
||||
socket?.emit("downloadFiles", {}, async (response: {files: {path: string, content: string}[]}) => {
|
||||
const zip = new JSZip()
|
||||
|
||||
response.files.forEach(file => {
|
||||
zip.file(file.path, file.content)
|
||||
})
|
||||
|
||||
const blob = await zip.generateAsync({type: "blob"})
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${name}.zip`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
}
|
||||
socket?.emit(
|
||||
"downloadFiles",
|
||||
{ timestamp: Date.now() },
|
||||
async (response: { zipBlob: string }) => {
|
||||
const { zipBlob } = response
|
||||
|
||||
// Decode Base64 back to binary data
|
||||
const binary = atob(zipBlob)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i)
|
||||
}
|
||||
const blob = new Blob([bytes], { type: "application/zip" })
|
||||
|
||||
// Create URL and download
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `${name}.zip`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" onClick={handleDownload}>
|
||||
|
@ -9,12 +9,12 @@ import { Pencil, Users } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { Avatars } from "../live/avatars"
|
||||
// import { Avatars } from "../live/avatars"
|
||||
import DeployButtonModal from "./deploy"
|
||||
import DownloadButton from "./downloadButton"
|
||||
import EditSandboxModal from "./edit"
|
||||
import RunButtonModal from "./run"
|
||||
import ShareSandboxModal from "./share"
|
||||
import DownloadButton from "./downloadButton"
|
||||
|
||||
export default function Navbar({
|
||||
userData,
|
||||
@ -70,15 +70,15 @@ export default function Navbar({
|
||||
sandboxData={sandboxData}
|
||||
/>
|
||||
<div className="flex items-center h-full space-x-4">
|
||||
<Avatars />
|
||||
{/* <Avatars /> */}
|
||||
|
||||
{isOwner ? (
|
||||
<>
|
||||
<DeployButtonModal data={sandboxData} userData={userData} />
|
||||
<Button variant="outline" onClick={() => setIsShareOpen(true)}>
|
||||
{/* <Button variant="outline" onClick={() => setIsShareOpen(true)}>
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
</Button> */}
|
||||
<DownloadButton name={sandboxData.name} /></>
|
||||
) : null}
|
||||
<ThemeSwitcher />
|
||||
|
@ -7,6 +7,7 @@ import { Sandbox } from "@/lib/types"
|
||||
import { Play, StopCircle } from "lucide-react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { templateConfigs } from "@/lib/templates"
|
||||
|
||||
export default function RunButtonModal({
|
||||
isRunning,
|
||||
@ -34,7 +35,12 @@ export default function RunButtonModal({
|
||||
}
|
||||
}
|
||||
}, [terminals, isRunning])
|
||||
|
||||
// commands to run in the terminal
|
||||
const COMMANDS = {
|
||||
streamlit: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
php: "echo http://localhost:80 && npx vite",
|
||||
default: "npm run dev",
|
||||
} as const
|
||||
const handleRun = async () => {
|
||||
if (isRunning && lastCreatedTerminalRef.current) {
|
||||
await closeTerminal(lastCreatedTerminalRef.current)
|
||||
@ -42,10 +48,7 @@ export default function RunButtonModal({
|
||||
setIsPreviewCollapsed(true)
|
||||
previewPanelRef.current?.collapse()
|
||||
} else if (!isRunning && terminals.length < 4) {
|
||||
const command =
|
||||
sandboxData.type === "streamlit"
|
||||
? "./venv/bin/streamlit run main.py --server.runOnSave true"
|
||||
: "npm run dev"
|
||||
const command = templateConfigs[sandboxData.type]?.runCommand || "npm run dev"
|
||||
|
||||
try {
|
||||
// Create a new terminal with the appropriate command
|
||||
|
@ -143,11 +143,7 @@ export default function ShareSandboxModal({
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
{shared.map((user) => (
|
||||
<SharedUser
|
||||
key={user.id}
|
||||
user={user}
|
||||
sandboxId={data.id}
|
||||
/>
|
||||
<SharedUser key={user.id} user={user} sandboxId={data.id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +25,6 @@ export default function Sidebar({
|
||||
handleDeleteFolder,
|
||||
socket,
|
||||
setFiles,
|
||||
addNew,
|
||||
deletingFolderId,
|
||||
toggleAIChat,
|
||||
isAIChatOpen,
|
||||
@ -43,7 +42,6 @@ export default function Sidebar({
|
||||
handleDeleteFolder: (folder: TFolder) => void
|
||||
socket: Socket
|
||||
setFiles: (files: (TFile | TFolder)[]) => void
|
||||
addNew: (name: string, type: "file" | "folder") => void
|
||||
deletingFolderId: string
|
||||
toggleAIChat: () => void
|
||||
isAIChatOpen: boolean
|
||||
@ -93,7 +91,7 @@ export default function Sidebar({
|
||||
"moveFile",
|
||||
{
|
||||
fileId,
|
||||
folderId
|
||||
folderId,
|
||||
},
|
||||
(response: (TFolder | TFile)[]) => {
|
||||
setFiles(response)
|
||||
@ -176,7 +174,6 @@ export default function Sidebar({
|
||||
stopEditing={() => {
|
||||
setCreatingNew(null)
|
||||
}}
|
||||
addNew={addNew}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
@ -203,20 +200,18 @@ export default function Sidebar({
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"w-full justify-start text-sm font-normal h-8 px-2 mb-2 border-t",
|
||||
isAIChatOpen
|
||||
? "bg-muted-foreground/25 text-foreground"
|
||||
isAIChatOpen
|
||||
? "bg-muted-foreground/25 text-foreground"
|
||||
: "text-muted-foreground"
|
||||
)}
|
||||
onClick={toggleAIChat}
|
||||
aria-disabled={false}
|
||||
style={{ opacity: 1 }}
|
||||
>
|
||||
<MessageSquareMore
|
||||
<MessageSquareMore
|
||||
className={cn(
|
||||
"h-4 w-4 mr-2",
|
||||
isAIChatOpen
|
||||
? "text-indigo-500"
|
||||
: "text-indigo-500 opacity-70"
|
||||
isAIChatOpen ? "text-indigo-500" : "text-indigo-500 opacity-70"
|
||||
)}
|
||||
/>
|
||||
AI Chat
|
||||
|
@ -9,12 +9,10 @@ export default function New({
|
||||
socket,
|
||||
type,
|
||||
stopEditing,
|
||||
addNew,
|
||||
}: {
|
||||
socket: Socket
|
||||
type: "file" | "folder"
|
||||
stopEditing: () => void
|
||||
addNew: (name: string, type: "file" | "folder") => void
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@ -25,19 +23,9 @@ export default function New({
|
||||
const valid = validateName(name, "", type)
|
||||
if (valid.status) {
|
||||
if (type === "file") {
|
||||
socket.emit(
|
||||
"createFile",
|
||||
{ name },
|
||||
({ success }: { success: boolean }) => {
|
||||
if (success) {
|
||||
addNew(name, type)
|
||||
}
|
||||
}
|
||||
)
|
||||
socket.emit("createFile", { name })
|
||||
} else {
|
||||
socket.emit("createFolder", { name }, () => {
|
||||
addNew(name, type)
|
||||
})
|
||||
socket.emit("createFolder", { name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -694,4 +694,4 @@
|
||||
}
|
||||
],
|
||||
"encodedTokensColors": []
|
||||
}
|
||||
}
|
||||
|
@ -45,9 +45,14 @@ export default function Landing() {
|
||||
<h1 className="text-2xl font-medium text-center mt-16">
|
||||
A Collaborative + AI-Powered Code Environment
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-4 text-center ">
|
||||
{/* <p className="text-muted-foreground mt-4 text-center ">
|
||||
Sandbox is an open-source cloud-based code editing environment with
|
||||
custom AI code autocompletion and real-time collaboration.
|
||||
</p> */}
|
||||
<p className="text-muted-foreground mt-4 text-center ">
|
||||
A cloud-based code editor featuring real-time collaboration,
|
||||
intelligent code autocompletion, and an AI assistant to help you code
|
||||
faster and smarter.
|
||||
</p>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Link href="/sign-up">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@ -56,4 +56,4 @@ const AlertDescription = React.forwardRef<
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertDescription, AlertTitle }
|
||||
|
@ -6,4 +6,3 @@ import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@ -29,4 +29,4 @@ const TooltipContent = React.forwardRef<
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
|
@ -11,27 +11,85 @@ import { MAX_FREE_GENERATION } from "@/lib/constant"
|
||||
import { User } from "@/lib/types"
|
||||
import { useClerk } from "@clerk/nextjs"
|
||||
import {
|
||||
Crown,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
User as UserIcon,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
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">
|
||||
@ -40,20 +98,8 @@ export default function UserButton({ userData }: { userData: User }) {
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Sparkles className="size-4 mr-2 text-indigo-500" />
|
||||
<div className="w-full flex flex-col items-start text-sm">
|
||||
<span className="text-sm">{`AI Usage: ${userData.generations}/${MAX_FREE_GENERATION}`}</span>
|
||||
<div className="rounded-full w-full mt-1 h-1.5 overflow-hidden bg-secondary border border-muted-foreground">
|
||||
<div
|
||||
className="h-full bg-indigo-500 rounded-full"
|
||||
style={{
|
||||
width: `${(userData.generations * 100) / 1000}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link href={"/dashboard"}>
|
||||
<LayoutDashboard className="mr-2 size-4" />
|
||||
@ -68,6 +114,52 @@ export default function UserButton({ userData }: { userData: User }) {
|
||||
<DropdownMenuSeparator />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Sparkles className="size-4 mr-2 text-indigo-500" />
|
||||
<div className="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 rounded-full transition-all duration-300 ${
|
||||
usagePercentage > 90 ? 'bg-red-500' :
|
||||
usagePercentage > 75 ? 'bg-yellow-500' :
|
||||
tierInfo.color.replace('text-', 'bg-')
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(usagePercentage, 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* <DropdownMenuItem className="cursor-pointer">
|
||||
<Pencil className="mr-2 size-4" />
|
||||
<span>Edit Profile</span>
|
||||
@ -83,3 +175,4 @@ export default function UserButton({ userData }: { userData: User }) {
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
terminals,
|
||||
setTerminals,
|
||||
setActiveTerminalId,
|
||||
setClosingTerminal: () => { },
|
||||
setClosingTerminal: () => {},
|
||||
socket,
|
||||
activeTerminalId,
|
||||
})
|
||||
|
@ -16,7 +16,7 @@ export const projectTemplates: {
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
icon: "/project-icons/more.svg",
|
||||
description: "More coming soon, feel free to contribute on GitHub",
|
||||
description: "A simple HTML/JS project for building web apps",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
@ -33,4 +33,11 @@ export const projectTemplates: {
|
||||
description: "A faster way to build and share data apps",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "php",
|
||||
name: "PHP",
|
||||
description: "PHP development environment",
|
||||
icon: "/project-icons/php.svg",
|
||||
disabled: false
|
||||
},
|
||||
]
|
||||
|
@ -38,7 +38,7 @@
|
||||
"csl": "xml",
|
||||
"cson": "coffeescript",
|
||||
"csproj": "xml",
|
||||
"css":"css",
|
||||
"css": "css",
|
||||
"ct": "xml",
|
||||
"ctp": "php",
|
||||
"cxx": "cpp",
|
||||
|
248
frontend/lib/templates/index.ts
Normal file
248
frontend/lib/templates/index.ts
Normal file
@ -0,0 +1,248 @@
|
||||
export interface TemplateConfig {
|
||||
id: string
|
||||
name: string,
|
||||
runCommand: string,
|
||||
fileStructure: {
|
||||
[key: string]: {
|
||||
purpose: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
conventions: string[]
|
||||
dependencies?: {
|
||||
[key: string]: string
|
||||
}
|
||||
scripts?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
||||
reactjs: {
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"src/": {
|
||||
purpose: "source",
|
||||
description: "Contains all React components and application logic"
|
||||
},
|
||||
"src/components/": {
|
||||
purpose: "components",
|
||||
description: "Reusable React components"
|
||||
},
|
||||
"src/lib/": {
|
||||
purpose: "utilities",
|
||||
description: "Utility functions and shared code"
|
||||
},
|
||||
"src/App.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Main application component"
|
||||
},
|
||||
"src/index.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application entry point"
|
||||
},
|
||||
"src/index.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and index.html"
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration"
|
||||
},
|
||||
"vite.config.ts": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use functional components with hooks",
|
||||
"Follow React naming conventions (PascalCase for components)",
|
||||
"Keep components small and focused",
|
||||
"Use TypeScript for type safety"
|
||||
],
|
||||
dependencies: {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.441.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
}
|
||||
},
|
||||
// Next.js template config
|
||||
nextjs: {
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"pages/": {
|
||||
purpose: "routing",
|
||||
description: "Page components and API routes"
|
||||
},
|
||||
"pages/api/": {
|
||||
purpose: "api",
|
||||
description: "API route handlers"
|
||||
},
|
||||
"pages/_app.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application wrapper component"
|
||||
},
|
||||
"pages/index.tsx": {
|
||||
purpose: "page",
|
||||
description: "Homepage component"
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and files"
|
||||
},
|
||||
"styles/": {
|
||||
purpose: "styles",
|
||||
description: "CSS modules and global styles"
|
||||
},
|
||||
"styles/globals.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"styles/Home.module.css": {
|
||||
purpose: "styles",
|
||||
description: "Homepage-specific styles"
|
||||
},
|
||||
"next.config.js": {
|
||||
purpose: "config",
|
||||
description: "Next.js configuration"
|
||||
},
|
||||
"next-env.d.ts": {
|
||||
purpose: "types",
|
||||
description: "Next.js TypeScript declarations"
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use file-system based routing",
|
||||
"Keep API routes in pages/api",
|
||||
"Use CSS Modules for component styles",
|
||||
"Follow Next.js data fetching patterns"
|
||||
],
|
||||
dependencies: {
|
||||
"next": "^14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
}
|
||||
},
|
||||
// Streamlit template config
|
||||
streamlit: {
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
fileStructure: {
|
||||
"main.py": {
|
||||
purpose: "entry",
|
||||
description: "Main Streamlit application file"
|
||||
},
|
||||
"requirements.txt": {
|
||||
purpose: "dependencies",
|
||||
description: "Python package dependencies"
|
||||
},
|
||||
"Procfile": {
|
||||
purpose: "deployment",
|
||||
description: "Deployment configuration for hosting platforms"
|
||||
},
|
||||
"venv/": {
|
||||
purpose: "environment",
|
||||
description: "Python virtual environment directory"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use Streamlit components for UI",
|
||||
"Follow PEP 8 style guide",
|
||||
"Keep dependencies in requirements.txt",
|
||||
"Use virtual environment for isolation"
|
||||
],
|
||||
dependencies: {
|
||||
"streamlit": "^1.40.0",
|
||||
"altair": "^5.5.0"
|
||||
},
|
||||
scripts: {
|
||||
"start": "streamlit run main.py",
|
||||
"dev": "./venv/bin/streamlit run main.py --server.runOnSave true"
|
||||
}
|
||||
},
|
||||
// HTML template config
|
||||
vanillajs: {
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"index.html": {
|
||||
purpose: "entry",
|
||||
description: "Main HTML entry point"
|
||||
},
|
||||
"style.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"script.js": {
|
||||
purpose: "scripts",
|
||||
description: "JavaScript application logic"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions"
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use semantic HTML elements",
|
||||
"Keep CSS modular and organized",
|
||||
"Write clean, modular JavaScript",
|
||||
"Follow modern ES6+ practices"
|
||||
],
|
||||
dependencies: {
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
||||
}
|
@ -83,8 +83,8 @@ export const closeTerminal = ({
|
||||
? numTerminals === 1
|
||||
? null
|
||||
: index < numTerminals - 1
|
||||
? terminals[index + 1].id
|
||||
: terminals[index - 1].id
|
||||
? terminals[index + 1].id
|
||||
: terminals[index - 1].id
|
||||
: activeTerminalId
|
||||
|
||||
setTerminals((prev) => prev.filter((t) => t.id !== term.id))
|
||||
|
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",
|
||||
},
|
||||
}
|
@ -74,10 +74,10 @@ function mapModule(module: string): monaco.languages.typescript.ModuleKind {
|
||||
}
|
||||
|
||||
function mapJSX(jsx: string | undefined): monaco.languages.typescript.JsxEmit {
|
||||
if (!jsx || typeof jsx !== 'string') {
|
||||
if (!jsx || typeof jsx !== "string") {
|
||||
return monaco.languages.typescript.JsxEmit.React // Default value
|
||||
}
|
||||
|
||||
|
||||
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
|
||||
preserve: monaco.languages.typescript.JsxEmit.Preserve,
|
||||
react: monaco.languages.typescript.JsxEmit.React,
|
||||
|
@ -10,6 +10,9 @@ export type User = {
|
||||
generations: number
|
||||
sandbox: Sandbox[]
|
||||
usersToSandboxes: UsersToSandboxes[]
|
||||
tier: "FREE" | "PRO" | "ENTERPRISE"
|
||||
tierExpiresAt: Date
|
||||
lastResetDate?: number
|
||||
}
|
||||
|
||||
export type Sandbox = {
|
||||
|
@ -1,82 +1,181 @@
|
||||
// Constants for username generation
|
||||
const WORDS = {
|
||||
adjectives: [
|
||||
"azure", "crimson", "golden", "silver", "violet", "emerald", "cobalt", "amber", "coral", "jade",
|
||||
"cyber", "digital", "quantum", "neural", "binary", "cosmic", "stellar", "atomic", "crypto", "nano",
|
||||
"swift", "brave", "clever", "wise", "noble", "rapid", "bright", "sharp", "keen", "bold",
|
||||
"dynamic", "epic", "mega", "ultra", "hyper", "super", "prime", "elite", "alpha", "omega",
|
||||
"pixel", "vector", "sonic", "laser", "matrix", "nexus", "proxy", "cloud", "data", "tech",
|
||||
],
|
||||
nouns: [
|
||||
"coder", "hacker", "dev", "ninja", "guru", "wizard", "admin", "mod", "chief", "boss",
|
||||
"wolf", "eagle", "phoenix", "dragon", "tiger", "falcon", "shark", "lion", "hawk", "bear",
|
||||
"byte", "bit", "node", "stack", "cache", "chip", "core", "net", "web", "app",
|
||||
"star", "nova", "pulsar", "comet", "nebula", "quasar", "cosmos", "orbit", "astro", "solar",
|
||||
"mind", "soul", "spark", "pulse", "force", "power", "wave", "storm", "flash", "surge",
|
||||
],
|
||||
prefixes: [
|
||||
"the", "mr", "ms", "dr", "pro", "master", "lord", "captain", "chief", "agent",
|
||||
],
|
||||
} as const;
|
||||
|
||||
// Helper function to get random element from array
|
||||
const getRandomElement = <T>(array: readonly T[]): T => {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
};
|
||||
|
||||
// Username pattern generators
|
||||
const usernamePatterns = {
|
||||
basic: (): string => {
|
||||
const adjective = getRandomElement(WORDS.adjectives);
|
||||
const noun = getRandomElement(WORDS.nouns);
|
||||
const number = Math.floor(Math.random() * 10000);
|
||||
return `${adjective}${noun}${number}`;
|
||||
},
|
||||
|
||||
prefixed: (): string => {
|
||||
const prefix = getRandomElement(WORDS.prefixes);
|
||||
const noun = getRandomElement(WORDS.nouns);
|
||||
const number = Math.floor(Math.random() * 100);
|
||||
return `${prefix}${noun}${number}`;
|
||||
},
|
||||
|
||||
doubleAdjective: (): string => {
|
||||
const adj1 = getRandomElement(WORDS.adjectives);
|
||||
const adj2 = getRandomElement(WORDS.adjectives);
|
||||
const noun = getRandomElement(WORDS.nouns);
|
||||
return `${adj1}${adj2}${noun}`;
|
||||
},
|
||||
|
||||
doubleNoun: (): string => {
|
||||
const noun1 = getRandomElement(WORDS.nouns);
|
||||
const noun2 = getRandomElement(WORDS.nouns);
|
||||
const number = Math.floor(Math.random() * 100);
|
||||
return `${noun1}${number}${noun2}`;
|
||||
},
|
||||
};
|
||||
|
||||
export function generateUsername(): string {
|
||||
const patterns = Object.values(usernamePatterns);
|
||||
const selectedPattern = getRandomElement(patterns);
|
||||
return selectedPattern();
|
||||
adjectives: [
|
||||
"azure",
|
||||
"crimson",
|
||||
"golden",
|
||||
"silver",
|
||||
"violet",
|
||||
"emerald",
|
||||
"cobalt",
|
||||
"amber",
|
||||
"coral",
|
||||
"jade",
|
||||
"cyber",
|
||||
"digital",
|
||||
"quantum",
|
||||
"neural",
|
||||
"binary",
|
||||
"cosmic",
|
||||
"stellar",
|
||||
"atomic",
|
||||
"crypto",
|
||||
"nano",
|
||||
"swift",
|
||||
"brave",
|
||||
"clever",
|
||||
"wise",
|
||||
"noble",
|
||||
"rapid",
|
||||
"bright",
|
||||
"sharp",
|
||||
"keen",
|
||||
"bold",
|
||||
"dynamic",
|
||||
"epic",
|
||||
"mega",
|
||||
"ultra",
|
||||
"hyper",
|
||||
"super",
|
||||
"prime",
|
||||
"elite",
|
||||
"alpha",
|
||||
"omega",
|
||||
"pixel",
|
||||
"vector",
|
||||
"sonic",
|
||||
"laser",
|
||||
"matrix",
|
||||
"nexus",
|
||||
"proxy",
|
||||
"cloud",
|
||||
"data",
|
||||
"tech",
|
||||
],
|
||||
nouns: [
|
||||
"coder",
|
||||
"hacker",
|
||||
"dev",
|
||||
"ninja",
|
||||
"guru",
|
||||
"wizard",
|
||||
"admin",
|
||||
"mod",
|
||||
"chief",
|
||||
"boss",
|
||||
"wolf",
|
||||
"eagle",
|
||||
"phoenix",
|
||||
"dragon",
|
||||
"tiger",
|
||||
"falcon",
|
||||
"shark",
|
||||
"lion",
|
||||
"hawk",
|
||||
"bear",
|
||||
"byte",
|
||||
"bit",
|
||||
"node",
|
||||
"stack",
|
||||
"cache",
|
||||
"chip",
|
||||
"core",
|
||||
"net",
|
||||
"web",
|
||||
"app",
|
||||
"star",
|
||||
"nova",
|
||||
"pulsar",
|
||||
"comet",
|
||||
"nebula",
|
||||
"quasar",
|
||||
"cosmos",
|
||||
"orbit",
|
||||
"astro",
|
||||
"solar",
|
||||
"mind",
|
||||
"soul",
|
||||
"spark",
|
||||
"pulse",
|
||||
"force",
|
||||
"power",
|
||||
"wave",
|
||||
"storm",
|
||||
"flash",
|
||||
"surge",
|
||||
],
|
||||
prefixes: [
|
||||
"the",
|
||||
"mr",
|
||||
"ms",
|
||||
"dr",
|
||||
"pro",
|
||||
"master",
|
||||
"lord",
|
||||
"captain",
|
||||
"chief",
|
||||
"agent",
|
||||
],
|
||||
} as const
|
||||
|
||||
// Helper function to get random element from array
|
||||
const getRandomElement = <T>(array: readonly T[]): T => {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
// Username pattern generators
|
||||
const usernamePatterns = {
|
||||
basic: (): string => {
|
||||
const adjective = getRandomElement(WORDS.adjectives)
|
||||
const noun = getRandomElement(WORDS.nouns)
|
||||
const number = Math.floor(Math.random() * 10000)
|
||||
return `${adjective}${noun}${number}`
|
||||
},
|
||||
|
||||
prefixed: (): string => {
|
||||
const prefix = getRandomElement(WORDS.prefixes)
|
||||
const noun = getRandomElement(WORDS.nouns)
|
||||
const number = Math.floor(Math.random() * 100)
|
||||
return `${prefix}${noun}${number}`
|
||||
},
|
||||
|
||||
doubleAdjective: (): string => {
|
||||
const adj1 = getRandomElement(WORDS.adjectives)
|
||||
const adj2 = getRandomElement(WORDS.adjectives)
|
||||
const noun = getRandomElement(WORDS.nouns)
|
||||
return `${adj1}${adj2}${noun}`
|
||||
},
|
||||
|
||||
doubleNoun: (): string => {
|
||||
const noun1 = getRandomElement(WORDS.nouns)
|
||||
const noun2 = getRandomElement(WORDS.nouns)
|
||||
const number = Math.floor(Math.random() * 100)
|
||||
return `${noun1}${number}${noun2}`
|
||||
},
|
||||
}
|
||||
|
||||
export function generateUsername(): string {
|
||||
const patterns = Object.values(usernamePatterns)
|
||||
const selectedPattern = getRandomElement(patterns)
|
||||
return selectedPattern()
|
||||
}
|
||||
|
||||
export async function generateUniqueUsername(
|
||||
checkExists: (username: string) => Promise<boolean>
|
||||
): Promise<string> {
|
||||
const MAX_ATTEMPTS = 10
|
||||
let attempts = 0
|
||||
let username = generateUsername()
|
||||
|
||||
while ((await checkExists(username)) && attempts < MAX_ATTEMPTS) {
|
||||
username = generateUsername()
|
||||
attempts++
|
||||
}
|
||||
|
||||
export async function generateUniqueUsername(
|
||||
checkExists: (username: string) => Promise<boolean>
|
||||
): Promise<string> {
|
||||
const MAX_ATTEMPTS = 10;
|
||||
let attempts = 0;
|
||||
let username = generateUsername();
|
||||
|
||||
while (await checkExists(username) && attempts < MAX_ATTEMPTS) {
|
||||
username = generateUsername();
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (attempts >= MAX_ATTEMPTS) {
|
||||
// Add a large random number to ensure uniqueness
|
||||
username = generateUsername() + Math.floor(Math.random() * 1000000);
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
if (attempts >= MAX_ATTEMPTS) {
|
||||
// Add a large random number to ensure uniqueness
|
||||
username = generateUsername() + Math.floor(Math.random() * 1000000)
|
||||
}
|
||||
|
||||
return username
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { type ClassValue, clsx } from "clsx"
|
||||
// import { toast } from "sonner"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import fileExtToLang from "./file-extension-to-language.json"
|
||||
import { Sandbox, TFile, TFolder } from "./types"
|
||||
import { TFile, TFolder } from "./types"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@ -38,31 +38,6 @@ export function validateName(
|
||||
return { status: true, message: "" }
|
||||
}
|
||||
|
||||
export function addNew(
|
||||
name: string,
|
||||
type: "file" | "folder",
|
||||
setFiles: React.Dispatch<React.SetStateAction<(TFolder | TFile)[]>>,
|
||||
sandboxData: Sandbox
|
||||
) {
|
||||
if (type === "file") {
|
||||
setFiles((prev) => [
|
||||
...prev,
|
||||
{ id: `projects/${sandboxData.id}/${name}`, name, type: "file" },
|
||||
])
|
||||
} else {
|
||||
console.log("adding folder")
|
||||
setFiles((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `projects/${sandboxData.id}/${name}`,
|
||||
name,
|
||||
type: "folder",
|
||||
children: [],
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createClient } from "@liveblocks/client"
|
||||
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react"
|
||||
import { createLiveblocksContext, createRoomContext } from "@liveblocks/react"
|
||||
import YLiveblocksProvider from "@liveblocks/yjs"
|
||||
import { colors } from "./lib/colors"
|
||||
|
||||
|
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",
|
||||
@ -127,6 +128,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",
|
||||
@ -4165,6 +4214,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",
|
||||
@ -4177,6 +4238,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",
|
||||
@ -5093,6 +5166,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",
|
||||
@ -5253,6 +5335,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",
|
||||
@ -5261,6 +5349,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",
|
||||
@ -5617,6 +5727,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",
|
||||
|
7
frontend/public/project-icons/php.svg
Normal file
7
frontend/public/project-icons/php.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" fill="#8892BF"/>
|
||||
<path d="M14.4392 10H16.1192L15.6444 12.5242H17.154C17.9819 12.5419 18.5986 12.7269 19.0045 13.0793C19.4184 13.4316 19.5402 14.1014 19.3698 15.0881L18.5541 19.4889H16.8497L17.6288 15.2863C17.7099 14.8457 17.6856 14.533 17.5558 14.348C17.426 14.163 17.146 14.0705 16.7158 14.0705L15.3644 14.0573L14.3661 19.4889H12.6861L14.4392 10Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.74092 12.5243H10.0036C10.9612 12.533 11.6552 12.8327 12.0854 13.4229C12.5156 14.0132 12.6576 14.8193 12.5115 15.8414C12.4548 16.3085 12.3289 16.7665 12.1341 17.2159C11.9474 17.6652 11.6878 18.0704 11.355 18.4317C10.9491 18.8898 10.5149 19.1805 10.0523 19.304C9.58969 19.4274 9.11076 19.489 8.61575 19.489H7.15484L6.69222 22H5L6.74092 12.5243ZM7.43485 17.9956L8.16287 14.0441H8.40879C8.49815 14.0441 8.5914 14.0396 8.6888 14.0309C9.33817 14.0221 9.87774 14.0882 10.308 14.2291C10.7462 14.37 10.8923 14.9031 10.7462 15.8282C10.5678 16.9296 10.2186 17.5727 9.69926 17.7577C9.1799 17.934 8.53053 18.0176 7.75138 18.0088H7.58094C7.53224 18.0088 7.48355 18.0043 7.43485 17.9956Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4365 12.5243H21.1738L19.4329 22H21.1251L21.5878 19.489H23.0487C23.5437 19.489 24.0226 19.4274 24.4852 19.304C24.9479 19.1805 25.382 18.8898 25.7879 18.4317C26.1207 18.0704 26.3803 17.6652 26.567 17.2159C26.7618 16.7665 26.8877 16.3085 26.9444 15.8414C27.0905 14.8193 26.9486 14.0132 26.5183 13.4229C26.0881 12.8327 25.3942 12.533 24.4365 12.5243ZM22.5958 14.0441L21.8678 17.9956C21.9165 18.0043 21.9652 18.0088 22.0139 18.0088H22.1843C22.9635 18.0176 23.6128 17.934 24.1322 17.7577C24.6515 17.5727 25.0007 16.9296 25.1792 15.8282C25.3253 14.9031 25.1792 14.37 24.7409 14.2291C24.3107 14.0882 23.7711 14.0221 23.1217 14.0309C23.0243 14.0396 22.9311 14.0441 22.8417 14.0441H22.5958Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
5
frontend/react-syntax-highlighter.d.ts
vendored
5
frontend/react-syntax-highlighter.d.ts
vendored
@ -1,3 +1,2 @@
|
||||
declare module 'react-syntax-highlighter';
|
||||
declare module 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
declare module "react-syntax-highlighter"
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
|
Reference in New Issue
Block a user