chore: format frontend code

This commit is contained in:
Akhilesh Rangani 2024-11-17 12:35:56 -05:00 committed by James Murdza
parent 5216a9d897
commit 062e8d9226
30 changed files with 735 additions and 568 deletions

View File

@ -51,7 +51,11 @@ const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
} }
) )
const userData: User = await userRes.json() 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,
}
}) })
) )
@ -94,7 +98,9 @@ export default async function CodePage({ params }: { params: { id: string } }) {
<Navbar <Navbar
userData={userData} userData={userData}
sandboxData={sandboxData} sandboxData={sandboxData}
shared={shared as { id: string; name: string; avatarUrl: string }[]} shared={
shared as { id: string; name: string; avatarUrl: string }[]
}
/> />
<div className="w-screen flex grow"> <div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxData={sandboxData} /> <CodeEditor userData={userData} sandboxData={sandboxData} />

View File

@ -1,7 +1,7 @@
import { User } from "@/lib/types" import { User } from "@/lib/types"
import { generateUniqueUsername } from "@/lib/username-generator"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { generateUniqueUsername } from "@/lib/username-generator";
export default async function AppAuthLayout({ export default async function AppAuthLayout({
children, children,
@ -27,22 +27,24 @@ export default async function AppAuthLayout({
if (!dbUserJSON.id) { if (!dbUserJSON.id) {
// Try to get GitHub username if available // Try to get GitHub username if available
const githubUsername = user.externalAccounts.find( const githubUsername = user.externalAccounts.find(
account => account.provider === "github" (account) => account.provider === "github"
)?.username; )?.username
const username = githubUsername || await generateUniqueUsername(async (username) => { const username =
// Check if username exists in database githubUsername ||
const userCheck = await fetch( (await generateUniqueUsername(async (username) => {
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/check-username?username=${username}`, // Check if username exists in database
{ const userCheck = await fetch(
headers: { `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/check-username?username=${username}`,
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`, {
}, headers: {
} Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
) },
const exists = await userCheck.json() }
return exists.exists )
}); const exists = await userCheck.json()
return exists.exists
}))
const res = await fetch( const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`, `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
@ -64,11 +66,11 @@ export default async function AppAuthLayout({
) )
if (!res.ok) { if (!res.ok) {
const error = await res.text(); const error = await res.text()
console.error("Failed to create user:", error); console.error("Failed to create user:", error)
} else { } else {
const data = await res.json(); const data = await res.json()
console.log("User created successfully:", data); console.log("User created successfully:", data)
} }
} }

View File

@ -1,7 +1,7 @@
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { ThemeProvider } from "@/components/ui/theme-provider" import { ThemeProvider } from "@/components/ui/theme-provider"
import { PreviewProvider } from "@/context/PreviewContext" import { PreviewProvider } from "@/context/PreviewContext"
import { SocketProvider } from '@/context/SocketContext' import { SocketProvider } from "@/context/SocketContext"
import { ClerkProvider } from "@clerk/nextjs" import { ClerkProvider } from "@clerk/nextjs"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { GeistMono } from "geist/font/mono" import { GeistMono } from "geist/font/mono"

View File

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

View File

@ -6,12 +6,12 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { projectTemplates } from "@/lib/data"
import { ChevronRight } from "lucide-react" import { ChevronRight } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import Avatar from "../ui/avatar" import Avatar from "../ui/avatar"
import Button from "../ui/customButton" import Button from "../ui/customButton"
import { projectTemplates } from "@/lib/data"
export default function DashboardSharedWithMe({ export default function DashboardSharedWithMe({
shared, shared,
@ -47,7 +47,8 @@ export default function DashboardSharedWithMe({
<Image <Image
alt="" alt=""
src={ src={
projectTemplates.find((p) => p.id === sandbox.type)?.icon ?? "/project-icons/node.svg" projectTemplates.find((p) => p.id === sandbox.type)
?.icon ?? "/project-icons/node.svg"
} }
width={20} width={20}
height={20} height={20}
@ -58,7 +59,7 @@ export default function DashboardSharedWithMe({
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center"> <div className="flex items-center">
<Avatar <Avatar
name={sandbox.author} name={sandbox.author}
avatarUrl={sandbox.authorAvatarUrl} avatarUrl={sandbox.authorAvatarUrl}
className="mr-2" 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 { 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 { looksLikeCode } from "./lib/chatUtils"
import { ChatInputProps } from "./types" import { ALLOWED_FILE_TYPES, ChatInputProps } from "./types"
export default function ChatInput({ export default function ChatInput({
input, input,
@ -21,12 +20,11 @@ export default function ChatInput({
onRemoveTab, onRemoveTab,
textareaRef, textareaRef,
}: ChatInputProps) { }: ChatInputProps) {
// Auto-resize textarea as content changes // Auto-resize textarea as content changes
useEffect(() => { useEffect(() => {
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.style.height = 'auto' textareaRef.current.style.height = "auto"
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' textareaRef.current.style.height = textareaRef.current.scrollHeight + "px"
} }
}, [input]) }, [input])
@ -40,7 +38,11 @@ export default function ChatInput({
e.preventDefault() e.preventDefault()
handleSend(false) handleSend(false)
} }
} else if (e.key === "Backspace" && input === "" && contextTabs.length > 0) { } else if (
e.key === "Backspace" &&
input === "" &&
contextTabs.length > 0
) {
e.preventDefault() e.preventDefault()
// Remove the last context tab // Remove the last context tab
const lastTab = contextTabs[contextTabs.length - 1] const lastTab = contextTabs[contextTabs.length - 1]
@ -51,89 +53,92 @@ export default function ChatInput({
// Handle paste events for image and code // Handle paste events for image and code
const handlePaste = async (e: React.ClipboardEvent) => { const handlePaste = async (e: React.ClipboardEvent) => {
// Handle image paste // Handle image paste
const items = Array.from(e.clipboardData.items); const items = Array.from(e.clipboardData.items)
for (const item of items) { for (const item of items) {
if (item.type.startsWith('image/')) { if (item.type.startsWith("image/")) {
e.preventDefault(); e.preventDefault()
const file = item.getAsFile(); const file = item.getAsFile()
if (!file) continue; if (!file) continue
try { try {
// Convert image to base64 string for context tab title and timestamp // Convert image to base64 string for context tab title and timestamp
const reader = new FileReader(); const reader = new FileReader()
reader.onload = () => { reader.onload = () => {
const base64String = reader.result as string; const base64String = reader.result as string
addContextTab( addContextTab(
"image", "image",
`Image ${new Date().toLocaleTimeString('en-US', { `Image ${new Date()
hour12: true, .toLocaleTimeString("en-US", {
hour: '2-digit', hour12: true,
minute: '2-digit' hour: "2-digit",
}).replace(/(\d{2}):(\d{2})/, '$1:$2')}`, minute: "2-digit",
})
.replace(/(\d{2}):(\d{2})/, "$1:$2")}`,
base64String base64String
); )
}; }
reader.readAsDataURL(file); reader.readAsDataURL(file)
} catch (error) { } catch (error) {
console.error('Error processing pasted image:', error); console.error("Error processing pasted image:", error)
} }
return; return
} }
} }
// Get text from clipboard // Get text from clipboard
const text = e.clipboardData.getData('text'); const text = e.clipboardData.getData("text")
// If text doesn't contain newlines or doesn't look like code, let it paste normally // If text doesn't contain newlines or doesn't look like code, let it paste normally
if (!text || !text.includes('\n') || !looksLikeCode(text)) { if (!text || !text.includes("\n") || !looksLikeCode(text)) {
return; return
} }
e.preventDefault(); e.preventDefault()
const editor = editorRef.current; const editor = editorRef.current
const currentSelection = editor?.getSelection(); const currentSelection = editor?.getSelection()
const lines = text.split('\n'); 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 // 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 selection exists in editor, use file name and line numbers
if (currentSelection && !currentSelection.isEmpty()) { if (currentSelection && !currentSelection.isEmpty()) {
addContextTab( addContextTab(
"code", "code",
`${activeFileName} (${currentSelection.startLineNumber}-${currentSelection.endLineNumber})`, `${activeFileName} (${currentSelection.startLineNumber}-${currentSelection.endLineNumber})`,
text, text,
{ start: currentSelection.startLineNumber, end: currentSelection.endLineNumber } {
); start: currentSelection.startLineNumber,
return; end: currentSelection.endLineNumber,
}
)
return
} }
// If we have stored line range from a copy operation in the editor // If we have stored line range from a copy operation in the editor
if (lastCopiedRangeRef.current) { if (lastCopiedRangeRef.current) {
const range = lastCopiedRangeRef.current; const range = lastCopiedRangeRef.current
addContextTab( addContextTab(
"code", "code",
`${activeFileName} (${range.startLine}-${range.endLine})`, `${activeFileName} (${range.startLine}-${range.endLine})`,
text, text,
{ start: range.startLine, end: range.endLine } { start: range.startLine, end: range.endLine }
); )
return; return
} }
// For code pasted from outside the editor // For code pasted from outside the editor
addContextTab( addContextTab("code", `Pasted Code (1-${lines.length})`, text, {
"code", start: 1,
`Pasted Code (1-${lines.length})`, end: lines.length,
text, })
{ start: 1, end: lines.length } }
);
};
// Handle image upload from local machine via input // Handle image upload from local machine via input
const handleImageUpload = () => { const handleImageUpload = () => {
const input = document.createElement('input') const input = document.createElement("input")
input.type = 'file' input.type = "file"
input.accept = 'image/*' input.accept = "image/*"
input.onchange = (e) => { input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0] const file = (e.target as HTMLInputElement).files?.[0]
if (file) onImageUpload(file) if (file) onImageUpload(file)
@ -155,14 +160,16 @@ export default function ChatInput({
// Handle file upload from local machine via input // Handle file upload from local machine via input
const handleFileUpload = () => { const handleFileUpload = () => {
const input = document.createElement('input') const input = document.createElement("input")
input.type = 'file' input.type = "file"
input.accept = '.txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf' input.accept = ".txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf"
input.onchange = (e) => { input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0] const file = (e.target as HTMLInputElement).files?.[0]
if (file) { if (file) {
if (!(file.type in ALLOWED_FILE_TYPES)) { 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 return
} }
@ -223,17 +230,16 @@ export default function ChatInput({
<span className="hidden sm:inline">File</span> <span className="hidden sm:inline">File</span>
</Button> </Button>
{/* Render image upload button */} {/* Render image upload button */}
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 px-2 sm:px-3" className="h-6 px-2 sm:px-3"
onClick={handleImageUpload} onClick={handleImageUpload}
> >
<ImageIcon className="h-3 w-3 sm:mr-1" /> <ImageIcon className="h-3 w-3 sm:mr-1" />
<span className="hidden sm:inline">Image</span> <span className="hidden sm:inline">Image</span>
</Button> </Button>
</div> </div>
</div> </div>
) )
} }

View File

@ -3,9 +3,9 @@ import React, { useState } from "react"
import ReactMarkdown from "react-markdown" import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm" import remarkGfm from "remark-gfm"
import { Button } from "../../ui/button" import { Button } from "../../ui/button"
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
import ContextTabs from "./ContextTabs" import ContextTabs from "./ContextTabs"
import { createMarkdownComponents } from './lib/markdownComponents' import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
import { createMarkdownComponents } from "./lib/markdownComponents"
import { MessageProps } from "./types" import { MessageProps } from "./types"
export default function ChatMessage({ export default function ChatMessage({
@ -14,7 +14,6 @@ export default function ChatMessage({
setIsContextExpanded, setIsContextExpanded,
socket, socket,
}: MessageProps) { }: MessageProps) {
// State for expanded message index // State for expanded message index
const [expandedMessageIndex, setExpandedMessageIndex] = useState< const [expandedMessageIndex, setExpandedMessageIndex] = useState<
number | null number | null
@ -23,7 +22,7 @@ export default function ChatMessage({
// State for copied text // State for copied text
const [copiedText, setCopiedText] = useState<string | null>(null) const [copiedText, setCopiedText] = useState<string | null>(null)
// Render copy button for text content // Render copy button for text content
const renderCopyButton = (text: any) => ( const renderCopyButton = (text: any) => (
<Button <Button
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)} onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
@ -43,26 +42,26 @@ export default function ChatMessage({
const askAboutCode = (code: any) => { const askAboutCode = (code: any) => {
const contextString = stringifyContent(code) const contextString = stringifyContent(code)
const newContext = `Regarding this code:\n${contextString}` const newContext = `Regarding this code:\n${contextString}`
// Format timestamp to match chat message format (HH:MM PM) // 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, hour12: true,
hour: '2-digit', hour: "2-digit",
minute: '2-digit', minute: "2-digit",
}) })
// Instead of replacing context, append to it // Instead of replacing context, append to it
if (message.role === "assistant") { if (message.role === "assistant") {
// For assistant messages, create a new context tab with the response content and timestamp // For assistant messages, create a new context tab with the response content and timestamp
setContext(newContext, `AI Response (${timestamp})`, { setContext(newContext, `AI Response (${timestamp})`, {
start: 1, start: 1,
end: contextString.split('\n').length end: contextString.split("\n").length,
}) })
} else { } else {
// For user messages, create a new context tab with the selected content and timestamp // For user messages, create a new context tab with the selected content and timestamp
setContext(newContext, `User Chat (${timestamp})`, { setContext(newContext, `User Chat (${timestamp})`, {
start: 1, start: 1,
end: contextString.split('\n').length end: contextString.split("\n").length,
}) })
} }
setIsContextExpanded(false) setIsContextExpanded(false)
@ -127,7 +126,9 @@ export default function ChatMessage({
contextTabs={parseContextToTabs(message.context)} contextTabs={parseContextToTabs(message.context)}
onRemoveTab={() => {}} onRemoveTab={() => {}}
isExpanded={expandedMessageIndex === 0} 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" className="[&_div:first-child>div:first-child>div]:bg-[#0D0D0D] [&_button:first-child]:hidden [&_button:last-child]:hidden"
/> />
{expandedMessageIndex === 0 && ( {expandedMessageIndex === 0 && (
@ -153,7 +154,7 @@ export default function ChatMessage({
const updatedContext = `Regarding this code:\n${e.target.value}` const updatedContext = `Regarding this code:\n${e.target.value}`
setContext(updatedContext, "Selected Content", { setContext(updatedContext, "Selected Content", {
start: 1, 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" 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 */} {/* Render markdown content */}
{message.role === "assistant" ? ( {message.role === "assistant" ? (
<ReactMarkdown <ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
remarkPlugins={[remarkGfm]}
components={components}
>
{message.content} {message.content}
</ReactMarkdown> </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) { function parseContextToTabs(context: string) {
const sections = context.split(/(?=File |Code from )/) const sections = context.split(/(?=File |Code from )/)
return sections.map((section, index) => { return sections
const lines = section.trim().split('\n') .map((section, index) => {
const titleLine = lines[0] const lines = section.trim().split("\n")
let content = lines.slice(1).join('\n').trim() 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```$/, '') // 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 ') // Determine if the context is a file or code
const name = titleLine.replace(/^(File |Code from )/, '').replace(':', '') const isFile = titleLine.startsWith("File ")
const name = titleLine.replace(/^(File |Code from )/, "").replace(":", "")
return {
id: `context-${index}`, return {
type: isFile ? "file" as const : "code" as const, id: `context-${index}`,
name: name, type: isFile ? ("file" as const) : ("code" as const),
content: content name: name,
} content: content,
}).filter(tab => tab.content.length > 0) }
})
.filter((tab) => tab.content.length > 0)
} }

View File

@ -1,16 +1,15 @@
import { Plus, X, Image as ImageIcon, FileText } from "lucide-react" import { Input } from "@/components/ui/input"
import { useState } from "react"
import { Button } from "../../ui/button"
import { TFile, TFolder } from "@/lib/types"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover" } from "@/components/ui/popover"
import { Input } from "@/components/ui/input" import { TFile, TFolder } from "@/lib/types"
import { ContextTab } from "./types" import { FileText, Image as ImageIcon, Plus, X } from "lucide-react"
import { ContextTabsProps } from "./types" import { useState } from "react"
// Ignore certain folders and files from the file tree 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" import { ignoredFiles, ignoredFolders } from "./lib/ignored-paths"
export default function ContextTabs({ export default function ContextTabs({
@ -20,7 +19,6 @@ export default function ContextTabs({
files = [], files = [],
onFileSelect, onFileSelect,
}: ContextTabsProps & { className?: string }) { }: ContextTabsProps & { className?: string }) {
// State for preview tab // State for preview tab
const [previewTab, setPreviewTab] = useState<ContextTab | null>(null) const [previewTab, setPreviewTab] = useState<ContextTab | null>(null)
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
@ -28,9 +26,9 @@ export default function ContextTabs({
// Allow preview for images and code selections from editor // Allow preview for images and code selections from editor
const togglePreview = (tab: ContextTab) => { const togglePreview = (tab: ContextTab) => {
if (!tab.lineRange && tab.type !== "image") { if (!tab.lineRange && tab.type !== "image") {
return; return
} }
// Toggle preview for images and code selections from editor // Toggle preview for images and code selections from editor
if (previewTab?.id === tab.id) { if (previewTab?.id === tab.id) {
setPreviewTab(null) setPreviewTab(null)
@ -50,13 +48,21 @@ export default function ContextTabs({
// Get all files from the file tree to search for context // Get all files from the file tree to search for context
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => { const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
return items.reduce((acc: TFile[], item) => { return items.reduce((acc: TFile[], item) => {
// Add file if it's not ignored // Add file if it's not ignored
if (item.type === "file" && !ignoredFiles.some((pattern: string) => if (
item.name.endsWith(pattern.replace('*', '')) || item.name === pattern item.type === "file" &&
)) { !ignoredFiles.some(
(pattern: string) =>
item.name.endsWith(pattern.replace("*", "")) ||
item.name === pattern
)
) {
acc.push(item) acc.push(item)
// Add all files from folder if it's not ignored // Add all files from folder if it's not ignored
} else if (item.type === "folder" && !ignoredFolders.some((folder: string) => folder === item.name)) { } else if (
item.type === "folder" &&
!ignoredFolders.some((folder: string) => folder === item.name)
) {
acc.push(...getAllFiles(item.children)) acc.push(...getAllFiles(item.children))
} }
return acc return acc
@ -65,22 +71,18 @@ export default function ContextTabs({
// Get all files from the file tree to search for context when adding context // Get all files from the file tree to search for context when adding context
const allFiles = getAllFiles(files) const allFiles = getAllFiles(files)
const filteredFiles = allFiles.filter(file => const filteredFiles = allFiles.filter((file) =>
file.name.toLowerCase().includes(searchQuery.toLowerCase()) file.name.toLowerCase().includes(searchQuery.toLowerCase())
) )
return ( return (
<div className={`border-none ${className || ''}`}> <div className={`border-none ${className || ""}`}>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center gap-1 overflow-hidden mb-2 flex-wrap"> <div className="flex items-center gap-1 overflow-hidden mb-2 flex-wrap">
{/* Add context tab button */} {/* Add context tab button */}
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button variant="ghost" size="icon" className="h-6 w-6">
variant="ghost"
size="icon"
className="h-6 w-6"
>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@ -143,20 +145,23 @@ export default function ContextTabs({
{previewTab && ( {previewTab && (
<div className="p-2 bg-input rounded-md max-h-[200px] overflow-auto mb-2"> <div className="p-2 bg-input rounded-md max-h-[200px] overflow-auto mb-2">
{previewTab.type === "image" ? ( {previewTab.type === "image" ? (
<img <img
src={previewTab.content} src={previewTab.content}
alt={previewTab.name} alt={previewTab.name}
className="max-w-full h-auto" className="max-w-full h-auto"
/> />
) : previewTab.lineRange && ( ) : (
<> previewTab.lineRange && (
<div className="text-xs text-muted-foreground mt-1"> <>
Lines {previewTab.lineRange.start}-{previewTab.lineRange.end} <div className="text-xs text-muted-foreground mt-1">
</div> Lines {previewTab.lineRange.start}-
<pre className="text-xs font-mono whitespace-pre-wrap"> {previewTab.lineRange.end}
{previewTab.content} </div>
</pre> <pre className="text-xs font-mono whitespace-pre-wrap">
</> {previewTab.content}
</pre>
</>
)
)} )}
{/* Render file context tab */} {/* Render file context tab */}
{previewTab.type === "file" && ( {previewTab.type === "file" && (
@ -169,4 +174,4 @@ export default function ContextTabs({
</div> </div>
</div> </div>
) )
} }

View File

@ -1,14 +1,14 @@
import { useSocket } from "@/context/SocketContext"
import { TFile } from "@/lib/types"
import { X } from "lucide-react" import { X } from "lucide-react"
import { nanoid } from "nanoid"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import LoadingDots from "../../ui/LoadingDots" import LoadingDots from "../../ui/LoadingDots"
import ChatInput from "./ChatInput" import ChatInput from "./ChatInput"
import ChatMessage from "./ChatMessage" import ChatMessage from "./ChatMessage"
import ContextTabs from "./ContextTabs" import ContextTabs from "./ContextTabs"
import { handleSend, handleStopGeneration } from "./lib/chatUtils" import { handleSend, handleStopGeneration } from "./lib/chatUtils"
import { nanoid } from 'nanoid' import { AIChatProps, ContextTab, Message } from "./types"
import { TFile } from "@/lib/types"
import { useSocket } from "@/context/SocketContext"
import { Message, ContextTab, AIChatProps } from './types'
export default function AIChat({ export default function AIChat({
activeFileContent, activeFileContent,
@ -56,47 +56,54 @@ export default function AIChat({
} }
// Add context tab to context tabs // 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 = { const newTab = {
id: nanoid(), id: nanoid(),
type: type as "file" | "code" | "image", type: type as "file" | "code" | "image",
name, name,
content, content,
lineRange lineRange,
} }
setContextTabs(prev => [...prev, newTab]) setContextTabs((prev) => [...prev, newTab])
} }
// Remove context tab from context tabs // Remove context tab from context tabs
const removeContextTab = (id: string) => { 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 // Add file to context tabs
const handleAddFile = (tab: ContextTab) => { 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 // Format code content to remove starting and ending code block markers if they exist
const formatCodeContent = (content: string) => { const formatCodeContent = (content: string) => {
return content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '') return content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
} }
// Get combined context from context tabs // Get combined context from context tabs
const getCombinedContext = () => { const getCombinedContext = () => {
if (contextTabs.length === 0) return '' if (contextTabs.length === 0) return ""
return contextTabs.map(tab => { return contextTabs
if (tab.type === 'file') { .map((tab) => {
const fileExt = tab.name.split('.').pop() || 'txt' if (tab.type === "file") {
const cleanContent = formatCodeContent(tab.content) const fileExt = tab.name.split(".").pop() || "txt"
return `File ${tab.name}:\n\`\`\`${fileExt}\n${cleanContent}\n\`\`\`` const cleanContent = formatCodeContent(tab.content)
} else if (tab.type === 'code') { return `File ${tab.name}:\n\`\`\`${fileExt}\n${cleanContent}\n\`\`\``
const cleanContent = formatCodeContent(tab.content) } else if (tab.type === "code") {
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\`` const cleanContent = formatCodeContent(tab.content)
} return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
return `${tab.name}:\n${tab.content}` }
}).join('\n\n') return `${tab.name}:\n${tab.content}`
})
.join("\n\n")
} }
// Handle sending message with context // Handle sending message with context
@ -120,9 +127,9 @@ export default function AIChat({
// Set context for the chat // Set context for the chat
const setContext = ( const setContext = (
context: string | null, context: string | null,
name: string, name: string,
range?: { start: number, end: number } range?: { start: number; end: number }
) => { ) => {
if (!context) { if (!context) {
setContextTabs([]) setContextTabs([])
@ -130,7 +137,7 @@ export default function AIChat({
} }
// Always add a new tab instead of updating existing ones // Always add a new tab instead of updating existing ones
addContextTab('code', name, context, range) addContextTab("code", name, context, range)
} }
return ( return (
@ -156,7 +163,7 @@ export default function AIChat({
className="flex-grow overflow-y-auto p-4 space-y-4" className="flex-grow overflow-y-auto p-4 space-y-4"
> >
{messages.map((message, messageIndex) => ( {messages.map((message, messageIndex) => (
// Render chat message component for each message // Render chat message component for each message
<ChatMessage <ChatMessage
key={messageIndex} key={messageIndex}
message={message} message={message}
@ -180,9 +187,9 @@ export default function AIChat({
socket={socket} socket={socket}
onFileSelect={(file: TFile) => { onFileSelect={(file: TFile) => {
socket?.emit("getFile", { fileId: file.id }, (response: string) => { 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\`\`\`` const formattedContent = `\`\`\`${fileExt}\n${response}\n\`\`\``
addContextTab('file', file.name, formattedContent) addContextTab("file", file.name, formattedContent)
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.focus() textareaRef.current.focus()
} }
@ -210,9 +217,9 @@ export default function AIChat({
}} }}
lastCopiedRangeRef={lastCopiedRangeRef} lastCopiedRangeRef={lastCopiedRangeRef}
activeFileName={activeFileName} activeFileName={activeFileName}
contextTabs={contextTabs.map(tab => ({ contextTabs={contextTabs.map((tab) => ({
...tab, ...tab,
title: tab.id title: tab.id,
}))} }))}
onRemoveTab={removeContextTab} onRemoveTab={removeContextTab}
/> />

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
// Stringify content for chat message component // Stringify content for chat message component
export const stringifyContent = ( export const stringifyContent = (
content: any, content: any,
seen = new WeakSet() seen = new WeakSet()
@ -66,19 +66,19 @@ export const stringifyContent = (
return String(content) return String(content)
} }
// Copy to clipboard for chat message component // Copy to clipboard for chat message component
export const copyToClipboard = ( export const copyToClipboard = (
text: string, text: string,
setCopiedText: (text: string | null) => void 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(() => { navigator.clipboard.writeText(text).then(() => {
setCopiedText(text) setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000) setTimeout(() => setCopiedText(null), 2000)
}) })
} }
// Handle send for chat message component // Handle send for chat message component
export const handleSend = async ( export const handleSend = async (
input: string, input: string,
context: string | null, context: string | null,
@ -92,24 +92,26 @@ export const handleSend = async (
activeFileContent: string activeFileContent: string
) => { ) => {
// Return if input is empty and context is null // 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 // Get timestamp for chat message component
const timestamp = new Date().toLocaleTimeString('en-US', { const timestamp = new Date()
hour12: true, .toLocaleTimeString("en-US", {
hour: '2-digit', hour12: true,
minute: '2-digit' hour: "2-digit",
}).replace(/(\d{2}):(\d{2})/, '$1:$2') 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 = { const userMessage = {
role: "user" as const, role: "user" as const,
content: input, content: input,
context: context || undefined, context: context || undefined,
timestamp: timestamp timestamp: timestamp,
} }
// Update messages for chat message component // Update messages for chat message component
const updatedMessages = [...messages, userMessage] const updatedMessages = [...messages, userMessage]
setMessages(updatedMessages) setMessages(updatedMessages)
setInput("") setInput("")
@ -120,13 +122,13 @@ export const handleSend = async (
abortControllerRef.current = new AbortController() abortControllerRef.current = new AbortController()
try { try {
// Create anthropic messages for chat message component // Create anthropic messages for chat message component
const anthropicMessages = updatedMessages.map((msg) => ({ const anthropicMessages = updatedMessages.map((msg) => ({
role: msg.role === "user" ? "human" : "assistant", role: msg.role === "user" ? "human" : "assistant",
content: msg.content, content: msg.content,
})) }))
// Fetch AI response for chat message component // Fetch AI response for chat message component
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, `${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
{ {
@ -148,19 +150,19 @@ export const handleSend = async (
throw new Error("Failed to get AI response") throw new Error("Failed to get AI response")
} }
// Get reader for chat message component // Get reader for chat message component
const reader = response.body?.getReader() const reader = response.body?.getReader()
const decoder = new TextDecoder() const decoder = new TextDecoder()
const assistantMessage = { role: "assistant" as const, content: "" } const assistantMessage = { role: "assistant" as const, content: "" }
setMessages([...updatedMessages, assistantMessage]) setMessages([...updatedMessages, assistantMessage])
setIsLoading(false) setIsLoading(false)
// Initialize buffer for chat message component // Initialize buffer for chat message component
let buffer = "" let buffer = ""
const updateInterval = 100 const updateInterval = 100
let lastUpdateTime = Date.now() let lastUpdateTime = Date.now()
// Read response from reader for chat message component // Read response from reader for chat message component
if (reader) { if (reader) {
while (true) { while (true) {
const { done, value } = await reader.read() const { done, value } = await reader.read()
@ -179,7 +181,7 @@ export const handleSend = async (
} }
} }
// Update messages for chat message component // Update messages for chat message component
setMessages((prev) => { setMessages((prev) => {
const updatedMessages = [...prev] const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1] const lastMessage = updatedMessages[updatedMessages.length - 1]
@ -188,7 +190,7 @@ export const handleSend = async (
}) })
} }
} catch (error: any) { } catch (error: any) {
// Handle abort error for chat message component // Handle abort error for chat message component
if (error.name === "AbortError") { if (error.name === "AbortError") {
console.log("Generation aborted") console.log("Generation aborted")
} else { } else {
@ -206,7 +208,7 @@ export const handleSend = async (
} }
} }
// Handle stop generation for chat message component // Handle stop generation for chat message component
export const handleStopGeneration = ( export const handleStopGeneration = (
abortControllerRef: React.MutableRefObject<AbortController | null> abortControllerRef: React.MutableRefObject<AbortController | null>
) => { ) => {
@ -215,21 +217,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 => { export const looksLikeCode = (text: string): boolean => {
const codeIndicators = [ const codeIndicators = [
/^import\s+/m, // import statements /^import\s+/m, // import statements
/^function\s+/m, // function declarations /^function\s+/m, // function declarations
/^class\s+/m, // class declarations /^class\s+/m, // class declarations
/^const\s+/m, // const declarations /^const\s+/m, // const declarations
/^let\s+/m, // let declarations /^let\s+/m, // let declarations
/^var\s+/m, // var declarations /^var\s+/m, // var declarations
/[{}\[\]();]/, // common code syntax /[{}\[\]();]/, // common code syntax
/^\s*\/\//m, // comments /^\s*\/\//m, // comments
/^\s*\/\*/m, // multi-line comments /^\s*\/\*/m, // multi-line comments
/=>/, // arrow functions /=>/, // arrow functions
/^export\s+/m, // export statements /^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 = [ export const ignoredFolders = [
// Package managers // Package managers
'node_modules', "node_modules",
'venv', "venv",
'.env', ".env",
'env', "env",
'.venv', ".venv",
'virtualenv', "virtualenv",
'pip-wheel-metadata', "pip-wheel-metadata",
// Build outputs // Build outputs
'.next', ".next",
'dist', "dist",
'build', "build",
'out', "out",
'__pycache__', "__pycache__",
'.webpack', ".webpack",
'.serverless', ".serverless",
'storybook-static', "storybook-static",
// Version control // Version control
'.git', ".git",
'.svn', ".svn",
'.hg', // Mercurial ".hg", // Mercurial
// Cache and temp files // Cache and temp files
'.cache', ".cache",
'coverage', "coverage",
'tmp', "tmp",
'.temp', ".temp",
'.npm', ".npm",
'.pnpm', ".pnpm",
'.yarn', ".yarn",
'.eslintcache', ".eslintcache",
'.stylelintcache', ".stylelintcache",
// IDE specific // IDE specific
'.idea', ".idea",
'.vscode', ".vscode",
'.vs', ".vs",
'.sublime', ".sublime",
// Framework specific // Framework specific
'.streamlit', ".streamlit",
'.next', ".next",
'static', "static",
'.pytest_cache', ".pytest_cache",
'.nuxt', ".nuxt",
'.docusaurus', ".docusaurus",
'.remix', ".remix",
'.parcel-cache', ".parcel-cache",
'public/build', // Remix/Rails "public/build", // Remix/Rails
'.turbo', // Turborepo ".turbo", // Turborepo
// Logs // Logs
'logs', "logs",
'*.log', "*.log",
'npm-debug.log*', "npm-debug.log*",
'yarn-debug.log*', "yarn-debug.log*",
'yarn-error.log*', "yarn-error.log*",
'pnpm-debug.log*', "pnpm-debug.log*",
] as const; ] as const
export const ignoredFiles = [ export const ignoredFiles = [
'.DS_Store', ".DS_Store",
'.env.local', ".env.local",
'.env.development', ".env.development",
'.env.production', ".env.production",
'.env.test', ".env.test",
'.env*.local', ".env*.local",
'.gitignore', ".gitignore",
'.npmrc', ".npmrc",
'.yarnrc', ".yarnrc",
'.editorconfig', ".editorconfig",
'.prettierrc', ".prettierrc",
'.eslintrc', ".eslintrc",
'.browserslistrc', ".browserslistrc",
'tsconfig.tsbuildinfo', "tsconfig.tsbuildinfo",
'*.pyc', "*.pyc",
'*.pyo', "*.pyo",
'*.pyd', "*.pyd",
'*.so', "*.so",
'*.dll', "*.dll",
'*.dylib', "*.dylib",
'*.class', "*.class",
'*.exe', "*.exe",
'package-lock.json', "package-lock.json",
'yarn.lock', "yarn.lock",
'pnpm-lock.yaml', "pnpm-lock.yaml",
'composer.lock', "composer.lock",
'poetry.lock', "poetry.lock",
'Gemfile.lock', "Gemfile.lock",
'*.min.js', "*.min.js",
'*.min.css', "*.min.css",
'*.map', "*.map",
'*.chunk.*', "*.chunk.*",
'*.hot-update.*', "*.hot-update.*",
'.vercel', ".vercel",
'.netlify' ".netlify",
] as const; ] as const

View File

@ -1,21 +1,26 @@
import { CornerUpLeft } from "lucide-react"
import { Components } from "react-markdown" import { Components } from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
import { Button } from "../../../ui/button" import { Button } from "../../../ui/button"
import { CornerUpLeft } from "lucide-react"
import { stringifyContent } from "./chatUtils" import { stringifyContent } from "./chatUtils"
// Create markdown components for chat message component // Create markdown components for chat message component
export const createMarkdownComponents = ( export const createMarkdownComponents = (
renderCopyButton: (text: any) => JSX.Element, renderCopyButton: (text: any) => JSX.Element,
renderMarkdownElement: (props: any) => JSX.Element, renderMarkdownElement: (props: any) => JSX.Element,
askAboutCode: (code: any) => void askAboutCode: (code: any) => void
): Components => ({ ): Components => ({
code: ({ node, className, children, ...props }: { code: ({
node?: import('hast').Element, node,
className?: string, className,
children?: React.ReactNode, children,
[key: string]: any, ...props
}: {
node?: import("hast").Element
className?: string
children?: React.ReactNode
[key: string]: any
}) => { }) => {
const match = /language-(\w+)/.exec(className || "") const match = /language-(\w+)/.exec(className || "")
@ -55,25 +60,30 @@ export const createMarkdownComponents = (
</div> </div>
</div> </div>
) : ( ) : (
<code className={className} {...props}>{children}</code> <code className={className} {...props}>
{children}
</code>
) )
}, },
// Render markdown elements // Render markdown elements
p: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }), p: ({ node, children, ...props }) =>
h1: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }), renderMarkdownElement({ node, children, ...props }),
h2: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }), h1: ({ node, children, ...props }) =>
h3: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }), renderMarkdownElement({ node, children, ...props }),
h4: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }), h2: ({ node, children, ...props }) =>
h5: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }), renderMarkdownElement({ node, children, ...props }),
h6: ({ 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: (props) => (
<ul className="list-disc pl-6 mb-4 space-y-2"> <ul className="list-disc pl-6 mb-4 space-y-2">{props.children}</ul>
{props.children}
</ul>
), ),
ol: (props) => ( ol: (props) => (
<ol className="list-decimal pl-6 mb-4 space-y-2"> <ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>
{props.children}
</ol>
), ),
}) })

View File

@ -1,28 +1,28 @@
import * as monaco from 'monaco-editor'
import { TFile, TFolder } from "@/lib/types" 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 // Allowed file types for context tabs
export const ALLOWED_FILE_TYPES = { export const ALLOWED_FILE_TYPES = {
// Text files // Text files
'text/plain': true, "text/plain": true,
'text/markdown': true, "text/markdown": true,
'text/csv': true, "text/csv": true,
// Code files // Code files
'application/json': true, "application/json": true,
'text/javascript': true, "text/javascript": true,
'text/typescript': true, "text/typescript": true,
'text/html': true, "text/html": true,
'text/css': true, "text/css": true,
// Documents // Documents
'application/pdf': true, "application/pdf": true,
// Images // Images
'image/jpeg': true, "image/jpeg": true,
'image/png': true, "image/png": true,
'image/gif': true, "image/gif": true,
'image/webp': true, "image/webp": true,
'image/svg+xml': true, "image/svg+xml": true,
} as const; } as const
// Message interface // Message interface
export interface Message { export interface Message {
@ -45,8 +45,13 @@ export interface AIChatProps {
activeFileContent: string activeFileContent: string
activeFileName: string activeFileName: string
onClose: () => void onClose: () => void
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined> editorRef: React.MutableRefObject<
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null> monaco.editor.IStandaloneCodeEditor | undefined
>
lastCopiedRangeRef: React.MutableRefObject<{
startLine: number
endLine: number
} | null>
files: (TFile | TFolder)[] files: (TFile | TFolder)[]
} }
@ -58,11 +63,27 @@ export interface ChatInputProps {
handleSend: (useFullContext?: boolean) => void handleSend: (useFullContext?: boolean) => void
handleStopGeneration: () => void handleStopGeneration: () => void
onImageUpload: (file: File) => 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 activeFileName?: string
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined> editorRef: React.MutableRefObject<
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null> monaco.editor.IStandaloneCodeEditor | undefined
contextTabs: { id: string; type: string; title: string; content: string; lineRange?: { start: number; end: number } }[] >
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 onRemoveTab: (id: string) => void
textareaRef: React.RefObject<HTMLTextAreaElement> textareaRef: React.RefObject<HTMLTextAreaElement>
} }
@ -74,7 +95,11 @@ export interface MessageProps {
content: string content: string
context?: 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 setIsContextExpanded: (isExpanded: boolean) => void
socket: Socket | null socket: Socket | null
} }

View File

@ -72,7 +72,7 @@ export default function GenerateInput({
fileName: data.fileName, fileName: data.fileName,
code: data.code, code: data.code,
line: data.line, line: data.line,
instructions: regenerate ? currentPrompt : input instructions: regenerate ? currentPrompt : input,
}, },
(res: { response: string; success: boolean }) => { (res: { response: string; success: boolean }) => {
console.log("Generated code", res.response, res.success) console.log("Generated code", res.response, res.success)

View File

@ -173,7 +173,10 @@ export default function CodeEditor({
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null) const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
// Ref to store the last copied range in the editor to be used in the AIChat component // 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( const debouncedSetIsSelected = useRef(
debounce((value: boolean) => { debounce((value: boolean) => {
@ -260,14 +263,14 @@ export default function CodeEditor({
// Store the last copied range in the editor to be used in the AIChat component // Store the last copied range in the editor to be used in the AIChat component
editor.onDidChangeCursorSelection((e) => { editor.onDidChangeCursorSelection((e) => {
const selection = editor.getSelection(); const selection = editor.getSelection()
if (selection) { if (selection) {
lastCopiedRangeRef.current = { lastCopiedRangeRef.current = {
startLine: selection.startLineNumber, startLine: selection.startLineNumber,
endLine: selection.endLineNumber endLine: selection.endLineNumber,
}; }
} }
}); })
} }
// Call the function with your file structure // Call the function with your file structure
@ -658,7 +661,7 @@ export default function CodeEditor({
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => { } const onConnect = () => {}
const onDisconnect = () => { const onDisconnect = () => {
setTerminals([]) setTerminals([])
@ -786,8 +789,8 @@ export default function CodeEditor({
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId : activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id)) setTabs((prev) => prev.filter((t) => t.id !== id))
@ -853,9 +856,13 @@ export default function CodeEditor({
} }
const handleDeleteFile = (file: TFile) => { const handleDeleteFile = (file: TFile) => {
socket?.emit("deleteFile", { fileId: file.id }, (response: (TFolder | TFile)[]) => { socket?.emit(
setFiles(response) "deleteFile",
}) { fileId: file.id },
(response: (TFolder | TFile)[]) => {
setFiles(response)
}
)
closeTab(file.id) closeTab(file.id)
} }
@ -867,10 +874,14 @@ export default function CodeEditor({
closeTabs(response) closeTabs(response)
) )
socket?.emit("deleteFolder", { folderId: folder.id }, (response: (TFolder | TFile)[]) => { socket?.emit(
setFiles(response) "deleteFolder",
setDeletingFolderId("") { folderId: folder.id },
}) (response: (TFolder | TFile)[]) => {
setFiles(response)
setDeletingFolderId("")
}
)
} }
const togglePreviewPanel = () => { const togglePreviewPanel = () => {
@ -911,7 +922,7 @@ export default function CodeEditor({
<DisableAccessModal <DisableAccessModal
message={disableAccess.message} message={disableAccess.message}
open={disableAccess.isDisabled} open={disableAccess.isDisabled}
setOpen={() => { }} setOpen={() => {}}
/> />
<Loading /> <Loading />
</> </>
@ -953,8 +964,8 @@ export default function CodeEditor({
code: code:
(isSelected && editorRef?.getSelection() (isSelected && editorRef?.getSelection()
? editorRef ? editorRef
?.getModel() ?.getModel()
?.getValueInRange(editorRef?.getSelection()!) ?.getValueInRange(editorRef?.getSelection()!)
: editorRef?.getValue()) ?? "", : editorRef?.getValue()) ?? "",
line: generate.line, line: generate.line,
}} }}
@ -1086,62 +1097,62 @@ export default function CodeEditor({
</div> </div>
</> </>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? ( clerk.loaded ? (
<> <>
{provider && userInfo ? ( {provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} /> <Cursors yProvider={provider} userInfo={userInfo} />
) : null} ) : null}
<Editor <Editor
height="100%" height="100%"
language={editorLanguage} language={editorLanguage}
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
// If the new content is different from the cached content, update it // If the new content is different from the cached content, update it
if (value !== fileContents[activeFileId]) { if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? "") // Update the active file content setActiveFileContent(value ?? "") // Update the active file content
// Mark the file as unsaved by setting 'saved' to false // Mark the file as unsaved by setting 'saved' to false
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId tab.id === activeFileId
? { ...tab, saved: false } ? { ...tab, saved: false }
: tab : tab
)
) )
} else { )
// If the content matches the cached content, mark the file as saved } else {
setTabs((prev) => // If the content matches the cached content, mark the file as saved
prev.map((tab) => setTabs((prev) =>
tab.id === activeFileId prev.map((tab) =>
? { ...tab, saved: true } tab.id === activeFileId
: tab ? { ...tab, saved: true }
) : tab
) )
} )
}} }
options={{ }}
tabSize: 2, options={{
minimap: { tabSize: 2,
enabled: false, minimap: {
}, enabled: false,
padding: { },
bottom: 4, padding: {
top: 4, bottom: 4,
}, top: 4,
scrollBeyondLastLine: false, },
fixedOverflowWidgets: true, scrollBeyondLastLine: false,
fontFamily: "var(--font-geist-mono)", fixedOverflowWidgets: true,
}} fontFamily: "var(--font-geist-mono)",
theme={theme === "light" ? "vs" : "vs-dark"} }}
value={activeFileContent} theme={theme === "light" ? "vs" : "vs-dark"}
/> value={activeFileContent}
</> />
) : ( </>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> ) : (
<Loader2 className="animate-spin w-6 h-6 mr-3" /> <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
Waiting for Clerk to load... <Loader2 className="animate-spin w-6 h-6 mr-3" />
</div> Waiting for Clerk to load...
)} </div>
)}
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
@ -1151,10 +1162,10 @@ export default function CodeEditor({
isAIChatOpen && isHorizontalLayout isAIChatOpen && isHorizontalLayout
? "horizontal" ? "horizontal"
: isAIChatOpen : isAIChatOpen
? "vertical" ? "vertical"
: isHorizontalLayout : isHorizontalLayout
? "horizontal" ? "horizontal"
: "vertical" : "vertical"
} }
> >
<ResizablePanel <ResizablePanel

View File

@ -11,10 +11,10 @@ import Link from "next/link"
import { useState } from "react" import { useState } from "react"
import { Avatars } from "../live/avatars" import { Avatars } from "../live/avatars"
import DeployButtonModal from "./deploy" import DeployButtonModal from "./deploy"
import DownloadButton from "./downloadButton"
import EditSandboxModal from "./edit" import EditSandboxModal from "./edit"
import RunButtonModal from "./run" import RunButtonModal from "./run"
import ShareSandboxModal from "./share" import ShareSandboxModal from "./share"
import DownloadButton from "./downloadButton"
export default function Navbar({ export default function Navbar({
userData, userData,
@ -79,7 +79,8 @@ export default function Navbar({
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Share Share
</Button> </Button>
<DownloadButton name={sandboxData.name} /></> <DownloadButton name={sandboxData.name} />
</>
) : null} ) : null}
<ThemeSwitcher /> <ThemeSwitcher />
<UserButton userData={userData} /> <UserButton userData={userData} />

View File

@ -143,11 +143,7 @@ export default function ShareSandboxModal({
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
{shared.map((user) => ( {shared.map((user) => (
<SharedUser <SharedUser key={user.id} user={user} sandboxId={data.id} />
key={user.id}
user={user}
sandboxId={data.id}
/>
))} ))}
</div> </div>
</div> </div>

View File

@ -93,7 +93,7 @@ export default function Sidebar({
"moveFile", "moveFile",
{ {
fileId, fileId,
folderId folderId,
}, },
(response: (TFolder | TFile)[]) => { (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response)
@ -203,20 +203,18 @@ export default function Sidebar({
variant="ghost" variant="ghost"
className={cn( className={cn(
"w-full justify-start text-sm font-normal h-8 px-2 mb-2 border-t", "w-full justify-start text-sm font-normal h-8 px-2 mb-2 border-t",
isAIChatOpen isAIChatOpen
? "bg-muted-foreground/25 text-foreground" ? "bg-muted-foreground/25 text-foreground"
: "text-muted-foreground" : "text-muted-foreground"
)} )}
onClick={toggleAIChat} onClick={toggleAIChat}
aria-disabled={false} aria-disabled={false}
style={{ opacity: 1 }} style={{ opacity: 1 }}
> >
<MessageSquareMore <MessageSquareMore
className={cn( className={cn(
"h-4 w-4 mr-2", "h-4 w-4 mr-2",
isAIChatOpen isAIChatOpen ? "text-indigo-500" : "text-indigo-500 opacity-70"
? "text-indigo-500"
: "text-indigo-500 opacity-70"
)} )}
/> />
AI Chat AI Chat

View File

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

View File

@ -1,5 +1,5 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -56,4 +56,4 @@ const AlertDescription = React.forwardRef<
)) ))
AlertDescription.displayName = "AlertDescription" AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertDescription, AlertTitle }

View File

@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View File

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

View File

@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -29,4 +29,4 @@ const TooltipContent = React.forwardRef<
)) ))
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

View File

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

View File

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

View File

@ -83,8 +83,8 @@ export const closeTerminal = ({
? numTerminals === 1 ? numTerminals === 1
? null ? null
: index < numTerminals - 1 : index < numTerminals - 1
? terminals[index + 1].id ? terminals[index + 1].id
: terminals[index - 1].id : terminals[index - 1].id
: activeTerminalId : activeTerminalId
setTerminals((prev) => prev.filter((t) => t.id !== term.id)) setTerminals((prev) => prev.filter((t) => t.id !== term.id))

View File

@ -74,10 +74,10 @@ function mapModule(module: string): monaco.languages.typescript.ModuleKind {
} }
function mapJSX(jsx: string | undefined): monaco.languages.typescript.JsxEmit { 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 return monaco.languages.typescript.JsxEmit.React // Default value
} }
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = { const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
preserve: monaco.languages.typescript.JsxEmit.Preserve, preserve: monaco.languages.typescript.JsxEmit.Preserve,
react: monaco.languages.typescript.JsxEmit.React, react: monaco.languages.typescript.JsxEmit.React,

View File

@ -1,82 +1,181 @@
// Constants for username generation // Constants for username generation
const WORDS = { const WORDS = {
adjectives: [ adjectives: [
"azure", "crimson", "golden", "silver", "violet", "emerald", "cobalt", "amber", "coral", "jade", "azure",
"cyber", "digital", "quantum", "neural", "binary", "cosmic", "stellar", "atomic", "crypto", "nano", "crimson",
"swift", "brave", "clever", "wise", "noble", "rapid", "bright", "sharp", "keen", "bold", "golden",
"dynamic", "epic", "mega", "ultra", "hyper", "super", "prime", "elite", "alpha", "omega", "silver",
"pixel", "vector", "sonic", "laser", "matrix", "nexus", "proxy", "cloud", "data", "tech", "violet",
], "emerald",
nouns: [ "cobalt",
"coder", "hacker", "dev", "ninja", "guru", "wizard", "admin", "mod", "chief", "boss", "amber",
"wolf", "eagle", "phoenix", "dragon", "tiger", "falcon", "shark", "lion", "hawk", "bear", "coral",
"byte", "bit", "node", "stack", "cache", "chip", "core", "net", "web", "app", "jade",
"star", "nova", "pulsar", "comet", "nebula", "quasar", "cosmos", "orbit", "astro", "solar", "cyber",
"mind", "soul", "spark", "pulse", "force", "power", "wave", "storm", "flash", "surge", "digital",
], "quantum",
prefixes: [ "neural",
"the", "mr", "ms", "dr", "pro", "master", "lord", "captain", "chief", "agent", "binary",
], "cosmic",
} as const; "stellar",
"atomic",
// Helper function to get random element from array "crypto",
const getRandomElement = <T>(array: readonly T[]): T => { "nano",
return array[Math.floor(Math.random() * array.length)]; "swift",
}; "brave",
"clever",
// Username pattern generators "wise",
const usernamePatterns = { "noble",
basic: (): string => { "rapid",
const adjective = getRandomElement(WORDS.adjectives); "bright",
const noun = getRandomElement(WORDS.nouns); "sharp",
const number = Math.floor(Math.random() * 10000); "keen",
return `${adjective}${noun}${number}`; "bold",
}, "dynamic",
"epic",
prefixed: (): string => { "mega",
const prefix = getRandomElement(WORDS.prefixes); "ultra",
const noun = getRandomElement(WORDS.nouns); "hyper",
const number = Math.floor(Math.random() * 100); "super",
return `${prefix}${noun}${number}`; "prime",
}, "elite",
"alpha",
doubleAdjective: (): string => { "omega",
const adj1 = getRandomElement(WORDS.adjectives); "pixel",
const adj2 = getRandomElement(WORDS.adjectives); "vector",
const noun = getRandomElement(WORDS.nouns); "sonic",
return `${adj1}${adj2}${noun}`; "laser",
}, "matrix",
"nexus",
doubleNoun: (): string => { "proxy",
const noun1 = getRandomElement(WORDS.nouns); "cloud",
const noun2 = getRandomElement(WORDS.nouns); "data",
const number = Math.floor(Math.random() * 100); "tech",
return `${noun1}${number}${noun2}`; ],
}, nouns: [
}; "coder",
"hacker",
export function generateUsername(): string { "dev",
const patterns = Object.values(usernamePatterns); "ninja",
const selectedPattern = getRandomElement(patterns); "guru",
return selectedPattern(); "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( if (attempts >= MAX_ATTEMPTS) {
checkExists: (username: string) => Promise<boolean> // Add a large random number to ensure uniqueness
): Promise<string> { username = generateUsername() + Math.floor(Math.random() * 1000000)
const MAX_ATTEMPTS = 10; }
let attempts = 0;
let username = generateUsername(); return username
}
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;
}

View File

@ -1,5 +1,5 @@
import { createClient } from "@liveblocks/client" import { createClient } from "@liveblocks/client"
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react" import { createLiveblocksContext, createRoomContext } from "@liveblocks/react"
import YLiveblocksProvider from "@liveblocks/yjs" import YLiveblocksProvider from "@liveblocks/yjs"
import { colors } from "./lib/colors" import { colors } from "./lib/colors"

View File

@ -1,3 +1,2 @@
declare module 'react-syntax-highlighter'; declare module "react-syntax-highlighter"
declare module 'react-syntax-highlighter/dist/esm/styles/prism'; declare module "react-syntax-highlighter/dist/esm/styles/prism"