Compare commits

..

28 Commits

Author SHA1 Message Date
6b2b870020 refactor: improve readability of the connection manager code 2024-10-25 19:27:32 -06:00
90ea90f610 fix: support filesystem change notifications for multiple connections 2024-10-25 19:27:30 -06:00
eb973e0f83 fix: call handlers without callbacks 2024-10-25 19:02:18 -06:00
6613291977 fix: only close the terminals and file manager when the owner is disconnected from all sockets 2024-10-25 14:14:50 -06:00
836dd51ccc refactor: improve naming 2024-10-25 07:36:43 -06:00
701c4fcf84 chore: add comments 2024-10-25 07:32:34 -06:00
8381455f4d refactor: separate connection manager logic 2024-10-25 07:32:34 -06:00
486791f53e refactor: simplify error handling 2024-10-25 07:32:34 -06:00
21026a3c53 fix: use correct path format for deployment 2024-10-25 07:32:34 -06:00
f83dcfcf8f refactor: simplify file list structure 2024-10-25 07:32:34 -06:00
250296f0e9 fix: correct usage of sandboxFiles 2024-10-25 07:32:34 -06:00
2eb2388e12 refactor: restructure error handling 2024-10-25 07:32:34 -06:00
a6f457ef59 refactor: move initialization code to SandboxManager 2024-10-25 07:32:28 -06:00
15fbd4ce41 refactor: create sandboxManager class 2024-10-25 00:03:04 -06:00
d6d9448aa4 refactor: export handlers as an object 2024-10-24 23:39:53 -06:00
d3e987b0ab refactor: move rate limiting to handler functions 2024-10-24 23:36:04 -06:00
3bc555ca47 refactor: fix handler arguments 2024-10-24 23:13:01 -06:00
6f8bebe7dd refactor: reuse try...catch and rate limiting code across handlers 2024-10-24 22:18:01 -06:00
0fe652d873 refactor: package websocket event arguments as objects 2024-10-24 20:00:50 -06:00
f1c1f50abf refactor: apply consistant callback usage 2024-10-24 19:15:03 -06:00
ca8c7ae0aa chore: change errors to warnings 2024-10-24 17:38:43 -06:00
f6cded11f4 refactor: move socket authentication middleware to a separate file 2024-10-24 17:37:34 -06:00
b1ada9e204 fix: type errors from refactoring 2024-10-24 17:37:12 -06:00
13c3670e4d refactor: pass context to handlers in handlerContext object 2024-10-24 17:15:58 -06:00
e439816671 refactor: keep disconnect handler in main file 2024-10-24 17:10:23 -06:00
ef018385ef refactor: move event logic to a separate file 2024-10-24 16:34:13 -06:00
cec6b0c8c5 refactor: separate socket event handlers into functions 2024-10-24 16:20:24 -06:00
6ec17fad7e refactor: move DokkuResponse to types 2024-10-24 15:59:21 -06:00
21 changed files with 249 additions and 2718 deletions

View File

@ -128,7 +128,7 @@ export class FileManager {
// Copy all files from the project to the container
const promises = this.fileData.map(async (file) => {
try {
const filePath = path.posix.join(this.dirName, file.id)
const filePath = path.join(this.dirName, file.id)
const parentDirectory = path.dirname(filePath)
if (!this.sandbox.files.exists(parentDirectory)) {
await this.sandbox.files.makeDir(parentDirectory)

View File

@ -8,7 +8,7 @@ import { AIWorker } from "./AIWorker"
import { ConnectionManager } from "./ConnectionManager"
import { DokkuClient } from "./DokkuClient"
import { Sandbox } from "./Sandbox"
import { Sandbox } from "./SandboxManager"
import { SecureGitClient } from "./SecureGitClient"
import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware
import { TFile, TFolder } from "./types"
@ -109,13 +109,13 @@ io.on("connection", async (socket) => {
try {
// Create or retrieve the sandbox manager for the given sandbox ID
const sandbox = sandboxes[data.sandboxId] ?? new Sandbox(
const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox(
data.sandboxId,
{
aiWorker, dokkuClient, gitClient,
}
)
sandboxes[data.sandboxId] = sandbox
sandboxes[data.sandboxId] = sandboxManager
// This callback recieves an update when the file list changes, and notifies all relevant connections.
const sendFileNotifications = (files: (TFolder | TFile)[]) => {
@ -126,13 +126,13 @@ io.on("connection", async (socket) => {
// Initialize the sandbox container
// The file manager and terminal managers will be set up if they have been closed
await sandbox.initialize(sendFileNotifications)
socket.emit("loaded", sandbox.fileManager?.files)
await sandboxManager.initialize(sendFileNotifications)
socket.emit("loaded", sandboxManager.fileManager?.files)
// Register event handlers for the sandbox
// For each event handler, listen on the socket for that event
// Pass connection-specific information to the handlers
Object.entries(sandbox.handlers({
Object.entries(sandboxManager.handlers({
userId: data.userId,
isOwner: data.isOwner,
socket
@ -156,7 +156,7 @@ io.on("connection", async (socket) => {
// If the owner has disconnected from all sockets, close open terminals and file watchers.o
// The sandbox itself will timeout after the heartbeat stops.
if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) {
await sandbox.disconnect()
await sandboxManager.disconnect()
socket.broadcast.emit(
"disableAccess",
"The sandbox owner has disconnected."

View File

@ -95,7 +95,7 @@ export default function Dashboard({
</Button> */}
</div>
<div className="flex flex-col">
<a target="_blank" href="https://github.com/jamesmurdza/sandbox">
<a target="_blank" href="https://github.com/ishaan1013/sandbox">
<Button
variant="ghost"
className="justify-start w-full font-normal text-muted-foreground"

View File

@ -1,10 +1,13 @@
import { Send, StopCircle, Image as ImageIcon, Paperclip } from "lucide-react"
import { Send, StopCircle } 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 { looksLikeCode } from "./lib/chatUtils"
import { ChatInputProps } from "./types"
interface ChatInputProps {
input: string
setInput: (input: string) => void
isGenerating: boolean
handleSend: () => void
handleStopGeneration: () => void
}
export default function ChatInput({
input,
@ -12,228 +15,37 @@ export default function ChatInput({
isGenerating,
handleSend,
handleStopGeneration,
onImageUpload,
addContextTab,
activeFileName,
editorRef,
lastCopiedRangeRef,
contextTabs,
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'
}
}, [input])
// Handle keyboard events for sending messages
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
if (e.ctrlKey) {
e.preventDefault()
handleSend(true) // Send with full context
} else if (!e.shiftKey && !isGenerating) {
e.preventDefault()
handleSend(false)
}
} else if (e.key === "Backspace" && input === "" && contextTabs.length > 0) {
e.preventDefault()
// Remove the last context tab
const lastTab = contextTabs[contextTabs.length - 1]
onRemoveTab(lastTab.id)
}
}
// Handle paste events for image and code
const handlePaste = async (e: React.ClipboardEvent) => {
// Handle image paste
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;
try {
// Convert image to base64 string for context tab title and timestamp
const reader = new FileReader();
reader.onload = () => {
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')}`,
base64String
);
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Error processing pasted image:', error);
}
return;
}
}
// 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;
}
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;
}
// If we have stored line range from a copy operation in the editor
if (lastCopiedRangeRef.current) {
const range = lastCopiedRangeRef.current;
addContextTab(
"code",
`${activeFileName} (${range.startLine}-${range.endLine})`,
text,
{ start: range.startLine, end: range.endLine }
);
return;
}
// For code pasted from outside the editor
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/*'
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) onImageUpload(file)
}
input.click()
}
// Helper function to flatten the file tree
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
return items.reduce((acc: TFile[], item) => {
if (item.type === "file") {
acc.push(item)
} else {
acc.push(...getAllFiles(item.children))
}
return acc
}, [])
}
// 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'
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.')
return
}
const reader = new FileReader()
reader.onload = () => {
addContextTab("file", file.name, reader.result as string)
}
reader.readAsText(file)
}
}
input.click()
}
return (
<div className="space-y-2">
<div className="flex space-x-2 min-w-0">
<textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
className="flex-grow p-2 border rounded-lg min-w-0 bg-input resize-none overflow-hidden"
placeholder="Type your message..."
disabled={isGenerating}
rows={1}
/>
{/* Render stop generation button */}
{isGenerating ? (
<Button
onClick={handleStopGeneration}
variant="destructive"
size="icon"
className="h-10 w-10"
>
<StopCircle className="w-4 h-4" />
</Button>
) : (
<Button
onClick={() => handleSend(false)}
disabled={isGenerating}
size="icon"
className="h-10 w-10"
>
<Send className="w-4 h-4" />
</Button>
)}
</div>
<div className="flex items-center justify-end gap-2">
{/* Render file upload button */}
<div className="flex space-x-2 min-w-0">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
placeholder="Type your message..."
disabled={isGenerating}
/>
{isGenerating ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 sm:px-3"
onClick={handleFileUpload}
onClick={handleStopGeneration}
variant="destructive"
size="icon"
className="h-10 w-10"
>
<Paperclip className="h-3 w-3 sm:mr-1" />
<span className="hidden sm:inline">File</span>
<StopCircle className="w-4 h-4" />
</Button>
{/* Render image upload button */}
) : (
<Button
variant="ghost"
size="sm"
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>
onClick={handleSend}
disabled={isGenerating}
size="icon"
className="h-10 w-10"
>
<Send className="w-4 h-4" />
</Button>
</div>
)}
</div>
)
}

View File

@ -1,29 +1,32 @@
import { Check, Copy, CornerUpLeft } from "lucide-react"
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
import React, { useState } from "react"
import ReactMarkdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
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 { MessageProps } from "./types"
interface MessageProps {
message: {
role: "user" | "assistant"
content: string
context?: string
}
setContext: (context: string | null) => void
setIsContextExpanded: (isExpanded: boolean) => void
}
export default function ChatMessage({
message,
setContext,
setIsContextExpanded,
socket,
}: MessageProps) {
// State for expanded message index
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
number | null
>(null)
// State for copied text
const [copiedText, setCopiedText] = useState<string | null>(null)
// Render copy button for text content
const renderCopyButton = (text: any) => (
<Button
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
@ -39,36 +42,12 @@ export default function ChatMessage({
</Button>
)
// Set context for code when asking about code
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', {
hour12: true,
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
})
} 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
})
}
setContext(`Regarding this code:\n${contextString}`)
setIsContextExpanded(false)
}
// Render markdown elements for code and text
const renderMarkdownElement = (props: any) => {
const { node, children } = props
const content = stringifyContent(children)
@ -86,7 +65,6 @@ export default function ChatMessage({
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
{/* Render markdown element */}
{React.createElement(
node.tagName,
{
@ -101,13 +79,6 @@ export default function ChatMessage({
)
}
// Create markdown components
const components = createMarkdownComponents(
renderCopyButton,
renderMarkdownElement,
askAboutCode
)
return (
<div className="text-left relative">
<div
@ -117,19 +88,34 @@ export default function ChatMessage({
: "bg-transparent text-white"
} max-w-full`}
>
{/* Render context tabs */}
{message.role === "user" && message.context && (
{message.role === "user" && (
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(message.content)}
<Button
onClick={() => askAboutCode(message.content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
)}
{message.context && (
<div className="mb-2 bg-input rounded-lg">
<ContextTabs
socket={socket}
activeFileName=""
onAddFile={() => {}}
contextTabs={parseContextToTabs(message.context)}
onRemoveTab={() => {}}
isExpanded={expandedMessageIndex === 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"
/>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() =>
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
}
>
<span className="text-sm text-gray-300">Context</span>
{expandedMessageIndex === 0 ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</div>
{expandedMessageIndex === 0 && (
<div className="relative">
<div className="absolute top-0 right-0 flex p-1">
@ -137,7 +123,6 @@ export default function ChatMessage({
message.context.replace(/^Regarding this code:\n/, "")
)}
</div>
{/* Render code textarea */}
{(() => {
const code = message.context.replace(
/^Regarding this code:\n/,
@ -151,10 +136,7 @@ export default function ChatMessage({
value={code}
onChange={(e) => {
const updatedContext = `Regarding this code:\n${e.target.value}`
setContext(updatedContext, "Selected Content", {
start: 1,
end: e.target.value.split('\n').length
})
setContext(updatedContext)
}}
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
rows={code.split("\n").length}
@ -171,25 +153,67 @@ export default function ChatMessage({
)}
</div>
)}
{/* Render copy and ask about code buttons */}
{message.role === "user" && (
<div className="absolute top-0 right-0 p-1 flex opacity-40">
{renderCopyButton(message.content)}
<Button
onClick={() => askAboutCode(message.content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
)}
{/* Render markdown content */}
{message.role === "assistant" ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={components}
components={{
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "")
return match ? (
<div className="relative border border-input rounded-md my-4">
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
{match[1]}
</div>
<div className="absolute top-0 right-0 flex">
{renderCopyButton(children)}
<Button
onClick={() => askAboutCode(children)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
<div className="pt-6">
<SyntaxHighlighter
style={vscDarkPlus as any}
language={match[1]}
PreTag="div"
customStyle={{
margin: 0,
padding: "0.5rem",
fontSize: "0.875rem",
}}
>
{stringifyContent(children)}
</SyntaxHighlighter>
</div>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
)
},
p: renderMarkdownElement,
h1: renderMarkdownElement,
h2: renderMarkdownElement,
h3: renderMarkdownElement,
h4: renderMarkdownElement,
h5: renderMarkdownElement,
h6: renderMarkdownElement,
ul: (props) => (
<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>
),
}}
>
{message.content}
</ReactMarkdown>
@ -200,27 +224,3 @@ export default function ChatMessage({
</div>
)
}
// 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)
}

View File

@ -0,0 +1,60 @@
import { ChevronDown, ChevronUp, X } from "lucide-react"
interface ContextDisplayProps {
context: string | null
isContextExpanded: boolean
setIsContextExpanded: (isExpanded: boolean) => void
setContext: (context: string | null) => void
}
export default function ContextDisplay({
context,
isContextExpanded,
setIsContextExpanded,
setContext,
}: ContextDisplayProps) {
if (!context) return null
return (
<div className="mb-2 bg-input p-2 rounded-lg">
<div className="flex justify-between items-center">
<div
className="flex-grow cursor-pointer"
onClick={() => setIsContextExpanded(!isContextExpanded)}
>
<span className="text-sm text-gray-300">Context</span>
</div>
<div className="flex items-center">
{isContextExpanded ? (
<ChevronUp
size={16}
className="cursor-pointer"
onClick={() => setIsContextExpanded(false)}
/>
) : (
<ChevronDown
size={16}
className="cursor-pointer"
onClick={() => setIsContextExpanded(true)}
/>
)}
<X
size={16}
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
onClick={() => setContext(null)}
/>
</div>
</div>
{isContextExpanded && (
<textarea
value={context.replace(/^Regarding this code:\n/, "")}
onChange={(e) =>
setContext(`Regarding this code:\n${e.target.value}`)
}
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
rows={5}
/>
)}
</div>
)
}

View File

@ -1,172 +0,0 @@
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 {
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 { ignoredFiles, ignoredFolders } from "./lib/ignored-paths"
export default function ContextTabs({
contextTabs,
onRemoveTab,
className,
files = [],
onFileSelect,
}: ContextTabsProps & { className?: string }) {
// State for preview tab
const [previewTab, setPreviewTab] = useState<ContextTab | null>(null)
const [searchQuery, setSearchQuery] = useState("")
// Allow preview for images and code selections from editor
const togglePreview = (tab: ContextTab) => {
if (!tab.lineRange && tab.type !== "image") {
return;
}
// Toggle preview for images and code selections from editor
if (previewTab?.id === tab.id) {
setPreviewTab(null)
} else {
setPreviewTab(tab)
}
}
// Remove tab from context when clicking on X
const handleRemoveTab = (id: string) => {
if (previewTab?.id === id) {
setPreviewTab(null)
}
onRemoveTab(id)
}
// 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
)) {
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)) {
acc.push(...getAllFiles(item.children))
}
return acc
}, [])
}
// Get all files from the file tree to search for context when adding context
const allFiles = getAllFiles(files)
const filteredFiles = allFiles.filter(file =>
file.name.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<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"
>
<Plus className="h-4 w-4" />
</Button>
</PopoverTrigger>
{/* Add context tab popover */}
<PopoverContent className="w-64 p-2">
<div className="flex gap-2 mb-2">
<Input
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1"
/>
</div>
<div className="max-h-[200px] overflow-y-auto">
{filteredFiles.map((file) => (
<Button
key={file.id}
variant="ghost"
className="w-full justify-start text-sm mb-1"
onClick={() => onFileSelect?.(file)}
>
<FileText className="h-4 w-4 mr-2" />
{file.name}
</Button>
))}
</div>
</PopoverContent>
</Popover>
{/* Add context tab button */}
{contextTabs.length === 0 && (
<div className="flex items-center gap-1 px-2 rounded">
<span className="text-sm text-muted-foreground">Add Context</span>
</div>
)}
{/* Render context tabs */}
{contextTabs.map((tab) => (
<div
key={tab.id}
className="flex items-center gap-1 px-2 bg-input rounded text-sm cursor-pointer hover:bg-muted"
onClick={() => togglePreview(tab)}
>
{tab.type === "image" && <ImageIcon className="h-3 w-3" />}
<span>{tab.name}</span>
<Button
variant="ghost"
size="icon"
className="h-4 w-4"
onClick={(e) => {
e.stopPropagation()
handleRemoveTab(tab.id)
}}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
{/* Preview Section */}
{previewTab && (
<div className="p-2 bg-input rounded-md max-h-[200px] overflow-auto mb-2">
{previewTab.type === "image" ? (
<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>
</>
)}
{/* Render file context tab */}
{previewTab.type === "file" && (
<pre className="text-xs font-mono whitespace-pre-wrap">
{previewTab.content}
</pre>
)}
</div>
)}
</div>
</div>
)
}

View File

@ -3,47 +3,37 @@ import { useEffect, useRef, useState } from "react"
import LoadingDots from "../../ui/LoadingDots"
import ChatInput from "./ChatInput"
import ChatMessage from "./ChatMessage"
import ContextTabs from "./ContextTabs"
import ContextDisplay from "./ContextDisplay"
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'
interface Message {
role: "user" | "assistant"
content: string
context?: string
}
export default function AIChat({
activeFileContent,
activeFileName,
onClose,
editorRef,
lastCopiedRangeRef,
files,
}: AIChatProps) {
// Initialize socket and messages
const { socket } = useSocket()
}: {
activeFileContent: string
activeFileName: string
onClose: () => void
}) {
const [messages, setMessages] = useState<Message[]>([])
// Initialize input and state for generating messages
const [input, setInput] = useState("")
const [isGenerating, setIsGenerating] = useState(false)
// Initialize chat container ref and abort controller ref
const chatContainerRef = useRef<HTMLDivElement>(null)
const abortControllerRef = useRef<AbortController | null>(null)
// Initialize context tabs and state for expanding context
const [contextTabs, setContextTabs] = useState<ContextTab[]>([])
const [context, setContext] = useState<string | null>(null)
const [isContextExpanded, setIsContextExpanded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
// Initialize textarea ref
const textareaRef = useRef<HTMLTextAreaElement>(null)
// Scroll to bottom of chat when messages change
useEffect(() => {
scrollToBottom()
}, [messages])
// Scroll to bottom of chat when messages change
const scrollToBottom = () => {
if (chatContainerRef.current) {
setTimeout(() => {
@ -55,84 +45,6 @@ export default function AIChat({
}
}
// Add context tab to context tabs
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
}
setContextTabs(prev => [...prev, newTab])
}
// Remove context tab from context tabs
const removeContextTab = (id: string) => {
setContextTabs(prev => prev.filter(tab => tab.id !== id))
}
// Add file to context tabs
const handleAddFile = (tab: ContextTab) => {
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```$/, '')
}
// 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')
}
// Handle sending message with context
const handleSendWithContext = () => {
const combinedContext = getCombinedContext()
handleSend(
input,
combinedContext,
messages,
setMessages,
setInput,
setIsContextExpanded,
setIsGenerating,
setIsLoading,
abortControllerRef,
activeFileContent
)
// Clear context tabs after sending
setContextTabs([])
}
// Set context for the chat
const setContext = (
context: string | null,
name: string,
range?: { start: number, end: number }
) => {
if (!context) {
setContextTabs([])
return
}
// Always add a new tab instead of updating existing ones
addContextTab('code', name, context, range)
}
return (
<div className="flex flex-col h-screen w-full">
<div className="flex justify-between items-center p-2 border-b">
@ -156,65 +68,41 @@ 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
<ChatMessage
key={messageIndex}
message={message}
setContext={setContext}
setIsContextExpanded={setIsContextExpanded}
socket={socket}
/>
))}
{isLoading && <LoadingDots />}
</div>
<div className="p-4 border-t mb-14">
{/* Render context tabs component */}
<ContextTabs
activeFileName={activeFileName}
onAddFile={handleAddFile}
contextTabs={contextTabs}
onRemoveTab={removeContextTab}
isExpanded={isContextExpanded}
onToggleExpand={() => setIsContextExpanded(!isContextExpanded)}
files={files}
socket={socket}
onFileSelect={(file: TFile) => {
socket?.emit("getFile", { fileId: file.id }, (response: string) => {
const fileExt = file.name.split('.').pop() || 'txt'
const formattedContent = `\`\`\`${fileExt}\n${response}\n\`\`\``
addContextTab('file', file.name, formattedContent)
if (textareaRef.current) {
textareaRef.current.focus()
}
})
}}
<ContextDisplay
context={context}
isContextExpanded={isContextExpanded}
setIsContextExpanded={setIsContextExpanded}
setContext={setContext}
/>
{/* Render chat input component */}
<ChatInput
textareaRef={textareaRef}
addContextTab={addContextTab}
editorRef={editorRef}
input={input}
setInput={setInput}
isGenerating={isGenerating}
handleSend={handleSendWithContext}
handleSend={() =>
handleSend(
input,
context,
messages,
setMessages,
setInput,
setIsContextExpanded,
setIsGenerating,
setIsLoading,
abortControllerRef,
activeFileContent
)
}
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
onImageUpload={(file) => {
const reader = new FileReader()
reader.onload = (e) => {
if (e.target?.result) {
addContextTab("image", file.name, e.target.result as string)
}
}
reader.readAsDataURL(file)
}}
lastCopiedRangeRef={lastCopiedRangeRef}
activeFileName={activeFileName}
contextTabs={contextTabs.map(tab => ({
...tab,
title: tab.id
}))}
onRemoveTab={removeContextTab}
/>
</div>
</div>

View File

@ -1,39 +1,30 @@
import React from "react"
// Stringify content for chat message component
export const stringifyContent = (
content: any,
seen = new WeakSet()
): string => {
// Stringify content if it's a string
if (typeof content === "string") {
return content
}
// Stringify content if it's null
if (content === null) {
return "null"
}
// Stringify content if it's undefined
if (content === undefined) {
return "undefined"
}
// Stringify content if it's a number or boolean
if (typeof content === "number" || typeof content === "boolean") {
return content.toString()
}
// Stringify content if it's a function
if (typeof content === "function") {
return content.toString()
}
// Stringify content if it's a symbol
if (typeof content === "symbol") {
return content.toString()
}
// Stringify content if it's a bigint
if (typeof content === "bigint") {
return content.toString() + "n"
}
// Stringify content if it's a valid React element
if (React.isValidElement(content)) {
return React.Children.toArray(
(content as React.ReactElement).props.children
@ -41,13 +32,11 @@ export const stringifyContent = (
.map((child) => stringifyContent(child, seen))
.join("")
}
// Stringify content if it's an array
if (Array.isArray(content)) {
return (
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
)
}
// Stringify content if it's an object
if (typeof content === "object") {
if (seen.has(content)) {
return "[Circular]"
@ -62,23 +51,19 @@ export const stringifyContent = (
return Object.prototype.toString.call(content)
}
}
// Stringify content if it's a primitive value
return String(content)
}
// 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
navigator.clipboard.writeText(text).then(() => {
setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000)
})
}
// Handle send for chat message component
export const handleSend = async (
input: string,
context: string | null,
@ -91,26 +76,14 @@ export const handleSend = async (
abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string
) => {
// Return if input is empty and context is null
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')
// Create user message for chat message component
const userMessage = {
const newMessage = {
role: "user" as const,
content: input,
context: context || undefined,
timestamp: timestamp
}
// Update messages for chat message component
const updatedMessages = [...messages, userMessage]
const updatedMessages = [...messages, newMessage]
setMessages(updatedMessages)
setInput("")
setIsContextExpanded(false)
@ -120,13 +93,11 @@ export const handleSend = async (
abortControllerRef.current = new AbortController()
try {
// Create anthropic messages for chat message component
const anthropicMessages = updatedMessages.map((msg) => ({
role: msg.role === "user" ? "human" : "assistant",
content: msg.content,
}))
// Fetch AI response for chat message component
const response = await fetch(
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
{
@ -143,24 +114,20 @@ export const handleSend = async (
}
)
// Throw error if response is not ok
if (!response.ok) {
throw new Error("Failed to get AI response")
}
// 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
let buffer = ""
const updateInterval = 100
let lastUpdateTime = Date.now()
// Read response from reader for chat message component
if (reader) {
while (true) {
const { done, value } = await reader.read()
@ -179,7 +146,6 @@ export const handleSend = async (
}
}
// Update messages for chat message component
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
@ -188,7 +154,6 @@ export const handleSend = async (
})
}
} catch (error: any) {
// Handle abort error for chat message component
if (error.name === "AbortError") {
console.log("Generation aborted")
} else {
@ -206,7 +171,6 @@ export const handleSend = async (
}
}
// Handle stop generation for chat message component
export const handleStopGeneration = (
abortControllerRef: React.MutableRefObject<AbortController | null>
) => {
@ -214,22 +178,3 @@ export const handleStopGeneration = (
abortControllerRef.current.abort()
}
}
// 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
];
return codeIndicators.some(pattern => pattern.test(text));
};

View File

@ -1,102 +0,0 @@
// 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;

View File

@ -1,79 +0,0 @@
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
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,
}) => {
const match = /language-(\w+)/.exec(className || "")
return match ? (
<div className="relative border border-input rounded-md my-4">
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
{match[1]}
</div>
<div className="absolute top-0 right-0 flex">
{renderCopyButton(children)}
<Button
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
askAboutCode(children)
}}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
<div className="pt-6">
<SyntaxHighlighter
style={vscDarkPlus as any}
language={match[1]}
PreTag="div"
customStyle={{
margin: 0,
padding: "0.5rem",
fontSize: "0.875rem",
}}
>
{stringifyContent(children)}
</SyntaxHighlighter>
</div>
</div>
) : (
<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 }),
ul: (props) => (
<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>
),
})

View File

@ -1,93 +0,0 @@
import * as monaco from 'monaco-editor'
import { TFile, TFolder } from "@/lib/types"
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,
// Code files
'application/json': true,
'text/javascript': true,
'text/typescript': true,
'text/html': true,
'text/css': true,
// Documents
'application/pdf': true,
// Images
'image/jpeg': true,
'image/png': true,
'image/gif': true,
'image/webp': true,
'image/svg+xml': true,
} as const;
// Message interface
export interface Message {
role: "user" | "assistant"
content: string
context?: string
}
// Context tab interface
export interface ContextTab {
id: string
type: "file" | "code" | "image"
name: string
content: string
lineRange?: { start: number; end: number }
}
// AIChat props interface
export interface AIChatProps {
activeFileContent: string
activeFileName: string
onClose: () => void
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
files: (TFile | TFolder)[]
}
// Chat input props interface
export interface ChatInputProps {
input: string
setInput: (input: string) => void
isGenerating: boolean
handleSend: (useFullContext?: boolean) => void
handleStopGeneration: () => void
onImageUpload: (file: File) => 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 } }[]
onRemoveTab: (id: string) => void
textareaRef: React.RefObject<HTMLTextAreaElement>
}
// Chat message props interface
export interface MessageProps {
message: {
role: "user" | "assistant"
content: string
context?: string
}
setContext: (context: string | null, name: string, range?: { start: number, end: number }) => void
setIsContextExpanded: (isExpanded: boolean) => void
socket: Socket | null
}
// Context tabs props interface
export interface ContextTabsProps {
activeFileName: string
onAddFile: (tab: ContextTab) => void
contextTabs: ContextTab[]
onRemoveTab: (id: string) => void
isExpanded: boolean
onToggleExpand: () => void
files?: (TFile | TFolder)[]
onFileSelect?: (file: TFile) => void
socket: Socket | null
}

View File

@ -107,6 +107,7 @@ export default function CodeEditor({
// Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext")
console.log("editor language: ", editorLanguage)
const [cursorLine, setCursorLine] = useState(0)
const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>()
@ -172,9 +173,6 @@ export default function CodeEditor({
const editorPanelRef = useRef<ImperativePanelHandle>(null)
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 debouncedSetIsSelected = useRef(
debounce((value: boolean) => {
setIsSelected(value)
@ -259,17 +257,6 @@ export default function CodeEditor({
updatedOptions
)
}
// Store the last copied range in the editor to be used in the AIChat component
editor.onDidChangeCursorSelection((e) => {
const selection = editor.getSelection();
if (selection) {
lastCopiedRangeRef.current = {
startLine: selection.startLineNumber,
endLine: selection.endLineNumber
};
}
});
}
// Call the function with your file structure
@ -1042,8 +1029,6 @@ export default function CodeEditor({
setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId}
toggleAIChat={toggleAIChat}
isAIChatOpen={isAIChatOpen}
/>
{/* Outer ResizablePanelGroup for main layout */}
<ResizablePanelGroup
@ -1233,9 +1218,6 @@ export default function CodeEditor({
"No file selected"
}
onClose={toggleAIChat}
editorRef={{ current: editorRef }}
lastCopiedRangeRef={lastCopiedRangeRef}
files={files}
/>
</ResizablePanel>
</>
@ -1246,17 +1228,4 @@ export default function CodeEditor({
)
}
/**
* Configure the typescript compiler to detect JSX and load type definitions
*/
const defaultCompilerOptions: monaco.languages.typescript.CompilerOptions = {
allowJs: true,
allowSyntheticDefaultImports: true,
allowNonTsExtensions: true,
resolveJsonModule: true,
jsx: monaco.languages.typescript.JsxEmit.ReactJSX,
module: monaco.languages.typescript.ModuleKind.ESNext,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
target: monaco.languages.typescript.ScriptTarget.ESNext,
}

View File

@ -10,7 +10,7 @@ import New from "./new"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { cn, sortFileExplorer } from "@/lib/utils"
import { sortFileExplorer } from "@/lib/utils"
import {
dropTargetForElements,
monitorForElements,
@ -27,8 +27,6 @@ export default function Sidebar({
setFiles,
addNew,
deletingFolderId,
toggleAIChat,
isAIChatOpen,
}: {
sandboxData: Sandbox
files: (TFile | TFolder)[]
@ -45,8 +43,6 @@ export default function Sidebar({
setFiles: (files: (TFile | TFolder)[]) => void
addNew: (name: string, type: "file" | "folder") => void
deletingFolderId: string
toggleAIChat: () => void
isAIChatOpen: boolean
}) {
const ref = useRef(null) // drop target
@ -192,7 +188,7 @@ export default function Sidebar({
style={{ opacity: 1 }}
>
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
AI Editor
Copilot
<div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
<span className="text-xs"></span>G
@ -201,24 +197,12 @@ export default function Sidebar({
</Button>
<Button
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"
: "text-muted-foreground"
)}
onClick={toggleAIChat}
aria-disabled={false}
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
disabled
aria-disabled="true"
style={{ opacity: 1 }}
>
<MessageSquareMore
className={cn(
"h-4 w-4 mr-2",
isAIChatOpen
? "text-indigo-500"
: "text-indigo-500 opacity-70"
)}
/>
<MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
AI Chat
<div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">

View File

@ -22,7 +22,7 @@ export default function Landing() {
</div>
<div className="flex items-center space-x-4">
<Button variant="outline" size="icon" asChild>
<a href="https://x.com/gitwitdev" target="_blank">
<a href="https://www.x.com/ishaandey_" target="_blank">
<svg
width="1200"
height="1227"
@ -54,7 +54,7 @@ export default function Landing() {
<CustomButton>Go To App</CustomButton>
</Link>
<a
href="https://github.com/jamesmurdza/sandbox"
href="https://github.com/ishaan1013/sandbox"
target="_blank"
className="group h-9 px-4 py-2 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
>

View File

@ -73,11 +73,7 @@ function mapModule(module: string): monaco.languages.typescript.ModuleKind {
)
}
function mapJSX(jsx: string | undefined): monaco.languages.typescript.JsxEmit {
if (!jsx || typeof jsx !== 'string') {
return monaco.languages.typescript.JsxEmit.React // Default value
}
function mapJSX(jsx: string): monaco.languages.typescript.JsxEmit {
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
preserve: monaco.languages.typescript.JsxEmit.Preserve,
react: monaco.languages.typescript.JsxEmit.React,

View File

@ -37,7 +37,6 @@
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"embla-carousel": "^8.3.0",
"embla-carousel-react": "^8.3.0",
"embla-carousel-wheel-gestures": "^8.0.1",
"framer-motion": "^11.2.3",
@ -3129,14 +3128,12 @@
"node_modules/embla-carousel": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz",
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==",
"license": "MIT"
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA=="
},
"node_modules/embla-carousel-react": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz",
"integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==",
"license": "MIT",
"dependencies": {
"embla-carousel": "8.3.0",
"embla-carousel-reactive-utils": "8.3.0"
@ -3157,7 +3154,6 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-8.0.1.tgz",
"integrity": "sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==",
"license": "MIT",
"dependencies": {
"wheel-gestures": "^2.2.5"
},

View File

@ -38,7 +38,6 @@
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"embla-carousel": "^8.3.0",
"embla-carousel-react": "^8.3.0",
"embla-carousel-wheel-gestures": "^8.0.1",
"framer-motion": "^11.2.3",

1669
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,5 @@
{
"dependencies": {
"@radix-ui/react-popover": "^1.1.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15"
}
}