feat: add AI edit code selection

This commit is contained in:
Hamzat Victor 2024-09-06 20:41:45 +01:00
parent 208d17879f
commit 62311faf51

View File

@ -1,10 +1,11 @@
"use client" "use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react" import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import monaco from "monaco-editor" import * as monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner" import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { AnimatePresence, motion } from "framer-motion"
import * as Y from "yjs" import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs"
@ -17,7 +18,7 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { FileJson, Loader2, TerminalSquare } from "lucide-react" import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
import Tab from "../ui/tab" import Tab from "../ui/tab"
import Sidebar from "./sidebar" import Sidebar from "./sidebar"
import GenerateInput from "./generate" import GenerateInput from "./generate"
@ -30,8 +31,10 @@ import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels" import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext'; import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext" import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button"
import React from "react"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -40,9 +43,8 @@ export default function CodeEditor({
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
}) { }) {
//SocketContext functions and effects //SocketContext functions and effects
const { socket, setUserAndSandboxId } = useSocket(); const { socket, setUserAndSandboxId } = useSocket()
useEffect(() => { useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect // Ensure userData.id and sandboxData.id are available before attempting to connect
@ -50,10 +52,10 @@ export default function CodeEditor({
// Check if the socket is not initialized or not connected // Check if the socket is not initialized or not connected
if (!socket || (socket && !socket.connected)) { if (!socket || (socket && !socket.connected)) {
// Initialize socket connection // Initialize socket connection
setUserAndSandboxId(userData.id, sandboxData.id); setUserAndSandboxId(userData.id, sandboxData.id)
} }
} }
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]); }, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -89,7 +91,8 @@ export default function CodeEditor({
options: monaco.editor.IModelDeltaDecoration[] options: monaco.editor.IModelDeltaDecoration[]
instance: monaco.editor.IEditorDecorationsCollection | undefined instance: monaco.editor.IEditorDecorationsCollection | undefined
}>({ options: [], instance: undefined }) }>({ options: [], instance: undefined })
const [isSelected, setIsSelected] = useState(false)
const [showSuggestion, setShowSuggestion] = useState(false)
// Terminal state // Terminal state
const [terminals, setTerminals] = useState< const [terminals, setTerminals] = useState<
{ {
@ -99,13 +102,13 @@ export default function CodeEditor({
>([]) >([])
// Preview state // Preview state
const [previewURL, setPreviewURL] = useState<string>(""); const [previewURL, setPreviewURL] = useState<string>("")
const loadPreviewURL = (url: string) => { const loadPreviewURL = (url: string) => {
// This will cause a reload if previewURL changed. // This will cause a reload if previewURL changed.
setPreviewURL(url); setPreviewURL(url)
// If the URL didn't change, still reload the preview. // If the URL didn't change, still reload the preview.
previewWindowRef.current?.refreshIframe(); previewWindowRef.current?.refreshIframe()
} }
const isOwner = sandboxData.userId === userData.id const isOwner = sandboxData.userId === userData.id
@ -118,23 +121,29 @@ export default function CodeEditor({
// Liveblocks providers map to prevent reinitializing providers // Liveblocks providers map to prevent reinitializing providers
type ProviderData = { type ProviderData = {
provider: LiveblocksProvider<never, never, never, never>; provider: LiveblocksProvider<never, never, never, never>
yDoc: Y.Doc; yDoc: Y.Doc
yText: Y.Text; yText: Y.Text
binding?: MonacoBinding; binding?: MonacoBinding
onSync: (isSynced: boolean) => void; onSync: (isSynced: boolean) => void
}; }
const providersMap = useRef(new Map<string, ProviderData>()); const providersMap = useRef(new Map<string, ProviderData>())
// Refs for libraries / features // Refs for libraries / features
const editorContainerRef = useRef<HTMLDivElement>(null) const editorContainerRef = useRef<HTMLDivElement>(null)
const monacoRef = useRef<typeof monaco | null>(null) const monacoRef = useRef<typeof monaco | null>(null)
const generateRef = useRef<HTMLDivElement>(null) const generateRef = useRef<HTMLDivElement>(null)
const suggestionRef = useRef<HTMLDivElement>(null)
const generateWidgetRef = useRef<HTMLDivElement>(null) const generateWidgetRef = useRef<HTMLDivElement>(null)
const previewPanelRef = useRef<ImperativePanelHandle>(null) const previewPanelRef = useRef<ImperativePanelHandle>(null)
const editorPanelRef = useRef<ImperativePanelHandle>(null) const editorPanelRef = useRef<ImperativePanelHandle>(null)
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null) const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
const debouncedSetIsSelected = useRef(
debounce((value: boolean) => {
setIsSelected(value)
}, 800) //
).current
// Pre-mount editor keybindings // Pre-mount editor keybindings
const handleEditorWillMount: BeforeMount = (monaco) => { const handleEditorWillMount: BeforeMount = (monaco) => {
monaco.editor.addKeybindingRules([ monaco.editor.addKeybindingRules([
@ -151,6 +160,13 @@ export default function CodeEditor({
monacoRef.current = monaco monacoRef.current = monaco
editor.onDidChangeCursorPosition((e) => { editor.onDidChangeCursorPosition((e) => {
setIsSelected(false)
const selection = editor.getSelection()
if (selection !== null) {
const hasSelection = !selection.isEmpty()
debouncedSetIsSelected(hasSelection)
setShowSuggestion(hasSelection)
}
const { column, lineNumber } = e.position const { column, lineNumber } = e.position
if (lineNumber === cursorLine) return if (lineNumber === cursorLine) return
setCursorLine(lineNumber) setCursorLine(lineNumber)
@ -204,7 +220,44 @@ export default function CodeEditor({
}, },
}) })
} }
const handleAiEdit = React.useCallback(() => {
if (!editorRef) return
const selection = editorRef.getSelection()
if (!selection) return
const pos = selection.getPosition()
const start = selection.getStartPosition()
const end = selection.getEndPosition()
let pref: monaco.editor.ContentWidgetPositionPreference
let id = ""
const isMultiline = start.lineNumber !== end.lineNumber
if (isMultiline) {
if (pos.lineNumber <= start.lineNumber) {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
} else {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
} else {
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
}
editorRef.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return
if (pref === monaco.editor.ContentWidgetPositionPreference.ABOVE) {
id = changeAccessor.addZone({
afterLineNumber: start.lineNumber - 1,
heightInLines: 2,
domNode: generateRef.current,
})
}
})
setGenerate((prev) => {
return {
...prev,
show: true,
pref: [pref],
id,
}
})
}, [editorRef])
// Generate widget effect // Generate widget effect
useEffect(() => { useEffect(() => {
if (!ai) { if (!ai) {
@ -217,15 +270,21 @@ export default function CodeEditor({
return return
} }
if (generate.show) { if (generate.show) {
setShowSuggestion(false)
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
if (!generateRef.current) return if (!generateRef.current) return
const id = changeAccessor.addZone({ if (!generate.id) {
afterLineNumber: cursorLine, const id = changeAccessor.addZone({
heightInLines: 3, afterLineNumber: cursorLine,
domNode: generateRef.current, heightInLines: 3,
}) domNode: generateRef.current,
})
setGenerate((prev) => {
return { ...prev, id, line: cursorLine }
})
}
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id, line: cursorLine } return { ...prev, line: cursorLine }
}) })
}) })
@ -286,6 +345,41 @@ export default function CodeEditor({
}) })
} }
}, [generate.show]) }, [generate.show])
// Suggestion widget effect
useEffect(() => {
if (!suggestionRef.current || !editorRef) return
const widgetElement = suggestionRef.current
const suggestionWidget: monaco.editor.IContentWidget = {
getDomNode: () => {
return widgetElement
},
getId: () => {
return "suggestion.widget"
},
getPosition: () => {
const selection = editorRef?.getSelection()
const column = Math.max(3, selection?.positionColumn ?? 1)
let lineNumber = selection?.positionLineNumber ?? 1
let pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
if (lineNumber <= 3) {
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
}
return {
preference: [pref],
position: {
lineNumber,
column,
},
}
},
}
if (isSelected) {
editorRef?.addContentWidget(suggestionWidget)
editorRef?.applyFontInfo(suggestionRef.current)
} else {
editorRef?.removeContentWidget(suggestionWidget)
}
}, [isSelected])
// Decorations effect for generate widget tips // Decorations effect for generate widget tips
useEffect(() => { useEffect(() => {
@ -325,27 +419,27 @@ export default function CodeEditor({
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
); )
console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${activeFileId}`)
console.log(`Saving file...${value}`); console.log(`Saving file...${value}`)
socket?.emit("saveFile", activeFileId, value); socket?.emit("saveFile", activeFileId, value)
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socket] [socket]
); )
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) { if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault()
debouncedSaveData(editorRef?.getValue(), activeFileId); debouncedSaveData(editorRef?.getValue(), activeFileId)
} }
}; }
document.addEventListener("keydown", down); document.addEventListener("keydown", down)
return () => { return () => {
document.removeEventListener("keydown", down); document.removeEventListener("keydown", down)
}; }
}, [activeFileId, tabs, debouncedSaveData]); }, [activeFileId, tabs, debouncedSaveData])
// Liveblocks live collaboration setup effect // Liveblocks live collaboration setup effect
useEffect(() => { useEffect(() => {
@ -354,13 +448,13 @@ export default function CodeEditor({
if (!editorRef || !tab || !model) return if (!editorRef || !tab || !model) return
let providerData: ProviderData; let providerData: ProviderData
// When a file is opened for the first time, create a new provider and store in providersMap. // When a file is opened for the first time, create a new provider and store in providersMap.
if (!providersMap.current.has(tab.id)) { if (!providersMap.current.has(tab.id)) {
const yDoc = new Y.Doc(); const yDoc = new Y.Doc()
const yText = yDoc.getText(tab.id); const yText = yDoc.getText(tab.id)
const yProvider = new LiveblocksProvider(room, yDoc); const yProvider = new LiveblocksProvider(room, yDoc)
// Inserts the file content into the editor once when the tab is changed. // Inserts the file content into the editor once when the tab is changed.
const onSync = (isSynced: boolean) => { const onSync = (isSynced: boolean) => {
@ -381,12 +475,11 @@ export default function CodeEditor({
yProvider.on("sync", onSync) yProvider.on("sync", onSync)
// Save the provider to the map. // Save the provider to the map.
providerData = { provider: yProvider, yDoc, yText, onSync }; providerData = { provider: yProvider, yDoc, yText, onSync }
providersMap.current.set(tab.id, providerData); providersMap.current.set(tab.id, providerData)
} else { } else {
// When a tab is opened that has been open before, reuse the existing provider. // When a tab is opened that has been open before, reuse the existing provider.
providerData = providersMap.current.get(tab.id)!; providerData = providersMap.current.get(tab.id)!
} }
const binding = new MonacoBinding( const binding = new MonacoBinding(
@ -394,21 +487,21 @@ export default function CodeEditor({
model, model,
new Set([editorRef]), new Set([editorRef]),
providerData.provider.awareness as unknown as Awareness providerData.provider.awareness as unknown as Awareness
); )
providerData.binding = binding; providerData.binding = binding
setProvider(providerData.provider); setProvider(providerData.provider)
return () => { return () => {
// Cleanup logic // Cleanup logic
if (binding) { if (binding) {
binding.destroy(); binding.destroy()
} }
if (providerData.binding) { if (providerData.binding) {
providerData.binding = undefined; providerData.binding = undefined
} }
}; }
}, [room, activeFileContent]); }, [room, activeFileContent])
// Added this effect to clean up when the component unmounts // Added this effect to clean up when the component unmounts
useEffect(() => { useEffect(() => {
@ -416,14 +509,14 @@ export default function CodeEditor({
// Clean up all providers when the component unmounts // Clean up all providers when the component unmounts
providersMap.current.forEach((data) => { providersMap.current.forEach((data) => {
if (data.binding) { if (data.binding) {
data.binding.destroy(); data.binding.destroy()
} }
data.provider.disconnect(); data.provider.disconnect()
data.yDoc.destroy(); data.yDoc.destroy()
}); })
providersMap.current.clear(); providersMap.current.clear()
}; }
}, []); }, [])
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
@ -435,7 +528,7 @@ export default function CodeEditor({
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => { } const onConnect = () => {}
const onDisconnect = () => { const onDisconnect = () => {
setTerminals([]) setTerminals([])
@ -481,48 +574,55 @@ export default function CodeEditor({
socket?.off("disableAccess", onDisableAccess) socket?.off("disableAccess", onDisableAccess)
socket?.off("previewURL", loadPreviewURL) socket?.off("previewURL", loadPreviewURL)
} }
}, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]); }, [
socket,
terminals,
setTerminals,
setFiles,
toast,
setDisableAccess,
isOwner,
loadPreviewURL,
])
// Helper functions for tabs: // Helper functions for tabs:
// Select file and load content // Select file and load content
// Initialize debounced function once // Initialize debounced function once
const fileCache = useRef(new Map()); const fileCache = useRef(new Map())
// Debounced function to get file content // Debounced function to get file content
const debouncedGetFile = const debouncedGetFile = (tabId: any, callback: any) => {
(tabId: any, callback: any) => { socket?.emit("getFile", tabId, callback)
socket?.emit('getFile', tabId, callback);
} // 300ms debounce delay, adjust as needed } // 300ms debounce delay, adjust as needed
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
if (tab.id === activeFileId) return
if (tab.id === activeFileId) return; setGenerate((prev) => ({ ...prev, show: false }))
setGenerate((prev) => ({ ...prev, show: false })); const exists = tabs.find((t) => t.id === tab.id)
const exists = tabs.find((t) => t.id === tab.id);
setTabs((prev) => { setTabs((prev) => {
if (exists) { if (exists) {
setActiveFileId(exists.id); setActiveFileId(exists.id)
return prev; return prev
} }
return [...prev, tab]; return [...prev, tab]
}); })
if (fileCache.current.has(tab.id)) { if (fileCache.current.has(tab.id)) {
setActiveFileContent(fileCache.current.get(tab.id)); setActiveFileContent(fileCache.current.get(tab.id))
} else { } else {
debouncedGetFile(tab.id, (response: SetStateAction<string>) => { debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
fileCache.current.set(tab.id, response); fileCache.current.set(tab.id, response)
setActiveFileContent(response); setActiveFileContent(response)
}); })
} }
setEditorLanguage(processFileType(tab.name)); setEditorLanguage(processFileType(tab.name))
setActiveFileId(tab.id); setActiveFileId(tab.id)
}; }
// Close tab and remove from tabs // Close tab and remove from tabs
const closeTab = (id: string) => { const closeTab = (id: string) => {
@ -538,8 +638,8 @@ export default function CodeEditor({
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId : activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id)) setTabs((prev) => prev.filter((t) => t.id !== id))
@ -632,7 +732,7 @@ export default function CodeEditor({
<DisableAccessModal <DisableAccessModal
message={disableAccess.message} message={disableAccess.message}
open={disableAccess.isDisabled} open={disableAccess.isDisabled}
setOpen={() => { }} setOpen={() => {}}
/> />
<Loading /> <Loading />
</> </>
@ -643,6 +743,23 @@ export default function CodeEditor({
{/* Copilot DOM elements */} {/* Copilot DOM elements */}
<PreviewProvider> <PreviewProvider>
<div ref={generateRef} /> <div ref={generateRef} />
<div ref={suggestionRef} className="absolute">
<AnimatePresence>
{isSelected && ai && showSuggestion && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: "easeOut", duration: 0.2 }}
>
<Button size="xs" type="submit" onClick={handleAiEdit}>
<Sparkles className="h-3 w-3 mr-1" />
Edit Code
</Button>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="z-50 p-1" ref={generateWidgetRef}> <div className="z-50 p-1" ref={generateWidgetRef}>
{generate.show && ai ? ( {generate.show && ai ? (
<GenerateInput <GenerateInput
@ -651,22 +768,49 @@ export default function CodeEditor({
width={generate.width - 90} width={generate.width - 90}
data={{ data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "", fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
code: editorRef?.getValue() ?? "", code:
(isSelected && editorRef?.getSelection()
? editorRef
?.getModel()
?.getValueInRange(editorRef?.getSelection()!)
: editorRef?.getValue()) ?? "",
line: generate.line, line: generate.line,
}} }}
editor={{ editor={{
language: editorLanguage, language: editorLanguage,
}} }}
onExpand={() => { onExpand={() => {
const line = generate.line
editorRef?.changeViewZones(function (changeAccessor) { editorRef?.changeViewZones(function (changeAccessor) {
changeAccessor.removeZone(generate.id) changeAccessor.removeZone(generate.id)
if (!generateRef.current) return if (!generateRef.current) return
const id = changeAccessor.addZone({ let id = ""
afterLineNumber: cursorLine, if (isSelected) {
heightInLines: 12, const selection = editorRef?.getSelection()
domNode: generateRef.current, if (!selection) return
}) const isAbove =
generate.pref?.[0] ===
monaco.editor.ContentWidgetPositionPreference.ABOVE
const afterLineNumber = isAbove ? line - 1 : line
id = changeAccessor.addZone({
afterLineNumber,
heightInLines: isAbove?11: 12,
domNode: generateRef.current,
})
const contentWidget= generate.widget
if (contentWidget){
editorRef?.layoutContentWidget(contentWidget)
}
} else {
id = changeAccessor.addZone({
afterLineNumber: cursorLine,
heightInLines: 12,
domNode: generateRef.current,
})
}
setGenerate((prev) => { setGenerate((prev) => {
return { ...prev, id } return { ...prev, id }
}) })
@ -680,12 +824,14 @@ export default function CodeEditor({
show: !prev.show, show: !prev.show,
} }
}) })
const file = editorRef?.getValue() const selection = editorRef?.getSelection()
const range =
const lines = file?.split("\n") || [] isSelected && selection
lines.splice(line - 1, 0, code) ? selection
const updatedFile = lines.join("\n") : new monaco.Range(line, 1, line, 1)
editorRef?.setValue(updatedFile) editorRef?.executeEdits("ai-generation", [
{ range, text: code, forceMoveMarkers: true },
])
}} }}
onClose={() => { onClose={() => {
setGenerate((prev) => { setGenerate((prev) => {
@ -754,58 +900,58 @@ export default function CodeEditor({
</div> </div>
</> </>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? ( clerk.loaded ? (
<> <>
{provider && userInfo ? ( {provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} /> <Cursors yProvider={provider} userInfo={userInfo} />
) : null} ) : null}
<Editor <Editor
height="100%" height="100%"
language={editorLanguage} language={editorLanguage}
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
if (value === activeFileContent) { if (value === activeFileContent) {
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId tab.id === activeFileId
? { ...tab, saved: true } ? { ...tab, saved: true }
: tab : tab
)
) )
} else { )
setTabs((prev) => } else {
prev.map((tab) => setTabs((prev) =>
tab.id === activeFileId prev.map((tab) =>
? { ...tab, saved: false } tab.id === activeFileId
: tab ? { ...tab, saved: false }
) : tab
) )
} )
}} }
options={{ }}
tabSize: 2, options={{
minimap: { tabSize: 2,
enabled: false, minimap: {
}, enabled: false,
padding: { },
bottom: 4, padding: {
top: 4, bottom: 4,
}, top: 4,
scrollBeyondLastLine: false, },
fixedOverflowWidgets: true, scrollBeyondLastLine: false,
fontFamily: "var(--font-geist-mono)", fixedOverflowWidgets: true,
}} fontFamily: "var(--font-geist-mono)",
theme="vs-dark" }}
value={activeFileContent} theme="vs-dark"
/> value={activeFileContent}
</> />
) : ( </>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> ) : (
<Loader2 className="animate-spin w-6 h-6 mr-3" /> <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
Waiting for Clerk to load... <Loader2 className="animate-spin w-6 h-6 mr-3" />
</div> Waiting for Clerk to load...
)} </div>
)}
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
@ -825,7 +971,11 @@ export default function CodeEditor({
open={() => { open={() => {
usePreview().previewPanelRef.current?.expand() usePreview().previewPanelRef.current?.expand()
setIsPreviewCollapsed(false) setIsPreviewCollapsed(false)
}} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} /> }}
collapsed={isPreviewCollapsed}
src={previewURL}
ref={previewWindowRef}
/>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel <ResizablePanel
@ -849,4 +999,3 @@ export default function CodeEditor({
</> </>
) )
} }