Merge branch 'main' into feat/profile-page

This commit is contained in:
Hamzat Victor Oluwabori
2024-11-25 23:10:21 +01:00
committed by GitHub
79 changed files with 2716 additions and 5837 deletions

View File

@ -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>
</>
)

View 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 }
)
}
}

View File

@ -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 })
}

View 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 }
)
}
}

View File

@ -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"

View File

@ -14,4 +14,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
}
}

View File

@ -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>

View File

@ -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>
</>

View File

@ -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"

View File

@ -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>
)
}

View File

@ -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)
}

View File

@ -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>
)
}
}

View File

@ -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}
/>

View File

@ -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))
}

View File

@ -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

View File

@ -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>
),
})

View File

@ -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
}

View File

@ -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>) => {

View File

@ -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>
</>

View File

@ -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}>

View File

@ -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 />

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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 })
}
}
}

View File

@ -694,4 +694,4 @@
}
],
"encodedTokensColors": []
}
}

View File

@ -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">

View File

@ -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 }

View File

@ -6,4 +6,3 @@ import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -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 }

View File

@ -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>
)
}

View File

@ -63,7 +63,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
terminals,
setTerminals,
setActiveTerminalId,
setClosingTerminal: () => { },
setClosingTerminal: () => {},
socket,
activeTerminalId,
})

View File

@ -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
},
]

View File

@ -38,7 +38,7 @@
"csl": "xml",
"cson": "coffeescript",
"csproj": "xml",
"css":"css",
"css": "css",
"ct": "xml",
"ctp": "php",
"cxx": "cpp",

View 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"
}
}
}

View File

@ -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
View 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",
},
}

View File

@ -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,

View File

@ -10,6 +10,9 @@ export type User = {
generations: number
sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[]
tier: "FREE" | "PRO" | "ENTERPRISE"
tierExpiresAt: Date
lastResetDate?: number
}
export type Sandbox = {

View File

@ -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
}

View File

@ -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

View File

@ -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"

View File

@ -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",

View File

@ -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",

View 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

View File

@ -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"