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()
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
userData={userData}
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">
<CodeEditor userData={userData} sandboxData={sandboxData} />

View File

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

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

@ -6,12 +6,12 @@ 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"
import Avatar from "../ui/avatar"
import Button from "../ui/customButton"
import { projectTemplates } from "@/lib/data"
export default function DashboardSharedWithMe({
shared,
@ -47,7 +47,8 @@ export default function DashboardSharedWithMe({
<Image
alt=""
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}
height={20}
@ -58,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 { useSocket } from "@/context/SocketContext"
import { TFile } from "@/lib/types"
import { X } 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,
@ -56,47 +56,54 @@ export default function AIChat({
}
// 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
@ -120,9 +127,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 +137,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 (
@ -156,7 +163,7 @@ export default function AIChat({
className="flex-grow overflow-y-auto p-4 space-y-4"
>
{messages.map((message, messageIndex) => (
// Render chat message component for each message
// Render chat message component for each message
<ChatMessage
key={messageIndex}
message={message}
@ -180,9 +187,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 +217,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,
@ -92,24 +92,26 @@ export const handleSend = async (
activeFileContent: 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,13 +122,13 @@ 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
// Fetch AI response for chat message component
const response = await fetch(
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
{
@ -148,19 +150,19 @@ export const handleSend = async (
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 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 +181,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,7 +190,7 @@ 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 {
@ -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 = (
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 => {
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,28 @@
import * as monaco from 'monaco-editor'
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,8 +45,13 @@ 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)[]
}
@ -58,11 +63,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 +95,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

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

View File

@ -173,7 +173,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 +263,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
@ -658,7 +661,7 @@ export default function CodeEditor({
// Socket event listener effect
useEffect(() => {
const onConnect = () => { }
const onConnect = () => {}
const onDisconnect = () => {
setTerminals([])
@ -786,8 +789,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 +856,13 @@ 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 },
(response: (TFolder | TFile)[]) => {
setFiles(response)
}
)
closeTab(file.id)
}
@ -867,10 +874,14 @@ 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)[]) => {
setFiles(response)
setDeletingFolderId("")
}
)
}
const togglePreviewPanel = () => {
@ -911,7 +922,7 @@ export default function CodeEditor({
<DisableAccessModal
message={disableAccess.message}
open={disableAccess.isDisabled}
setOpen={() => { }}
setOpen={() => {}}
/>
<Loading />
</>
@ -953,8 +964,8 @@ export default function CodeEditor({
code:
(isSelected && editorRef?.getSelection()
? editorRef
?.getModel()
?.getValueInRange(editorRef?.getSelection()!)
?.getModel()
?.getValueInRange(editorRef?.getSelection()!)
: editorRef?.getValue()) ?? "",
line: generate.line,
}}
@ -1086,62 +1097,62 @@ export default function CodeEditor({
</div>
</>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? (
<>
{provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} />
) : null}
<Editor
height="100%"
language={editorLanguage}
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
onChange={(value) => {
// If the new content is different from the cached content, update it
if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? "") // Update the active file content
// Mark the file as unsaved by setting 'saved' to false
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: false }
: tab
)
clerk.loaded ? (
<>
{provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} />
) : null}
<Editor
height="100%"
language={editorLanguage}
beforeMount={handleEditorWillMount}
onMount={handleEditorMount}
onChange={(value) => {
// If the new content is different from the cached content, update it
if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? "") // Update the active file content
// Mark the file as unsaved by setting 'saved' to false
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: false }
: tab
)
} else {
// If the content matches the cached content, mark the file as saved
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: true }
: tab
)
)
} else {
// If the content matches the cached content, mark the file as saved
setTabs((prev) =>
prev.map((tab) =>
tab.id === activeFileId
? { ...tab, saved: true }
: tab
)
}
}}
options={{
tabSize: 2,
minimap: {
enabled: false,
},
padding: {
bottom: 4,
top: 4,
},
scrollBeyondLastLine: false,
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
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" />
Waiting for Clerk to load...
</div>
)}
)
}
}}
options={{
tabSize: 2,
minimap: {
enabled: false,
},
padding: {
bottom: 4,
top: 4,
},
scrollBeyondLastLine: false,
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
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" />
Waiting for Clerk to load...
</div>
)}
</div>
</ResizablePanel>
<ResizableHandle />
@ -1151,10 +1162,10 @@ export default function CodeEditor({
isAIChatOpen && isHorizontalLayout
? "horizontal"
: isAIChatOpen
? "vertical"
: isHorizontalLayout
? "horizontal"
: "vertical"
? "vertical"
: isHorizontalLayout
? "horizontal"
: "vertical"
}
>
<ResizablePanel

View File

@ -11,10 +11,10 @@ import Link from "next/link"
import { useState } from "react"
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,
@ -79,7 +79,8 @@ export default function Navbar({
<Users className="w-4 h-4 mr-2" />
Share
</Button>
<DownloadButton name={sandboxData.name} /></>
<DownloadButton name={sandboxData.name} />
</>
) : null}
<ThemeSwitcher />
<UserButton userData={userData} />

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

@ -93,7 +93,7 @@ export default function Sidebar({
"moveFile",
{
fileId,
folderId
folderId,
},
(response: (TFolder | TFile)[]) => {
setFiles(response)
@ -203,20 +203,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

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

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

@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
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) {
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

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

View File

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

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

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

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

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

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