Merge branch 'refs/heads/main' into production

This commit is contained in:
James Murdza 2024-09-06 14:19:24 -07:00
commit 653142dd1d
4 changed files with 315 additions and 154 deletions

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react" import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client"
@ -59,7 +59,7 @@ export default function GenerateInput({
}: { }: {
regenerate?: boolean regenerate?: boolean
}) => { }) => {
if (user.generations >= 10) { if (user.generations >= 1000) {
toast.error("You reached the maximum # of generations.") toast.error("You reached the maximum # of generations.")
return return
} }
@ -84,6 +84,13 @@ export default function GenerateInput({
} }
) )
} }
const handleGenerateForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
handleGenerate({ regenerate: false })
},
[]
)
useEffect(() => { useEffect(() => {
if (code) { if (code) {
@ -106,7 +113,10 @@ export default function GenerateInput({
return ( return (
<div className="w-full pr-4 space-y-2"> <div className="w-full pr-4 space-y-2">
<div className="flex items-center font-sans space-x-2"> <form
onSubmit={handleGenerateForm}
className="flex items-center font-sans space-x-2"
>
<input <input
ref={inputRef} ref={inputRef}
style={{ style={{
@ -120,8 +130,8 @@ export default function GenerateInput({
<Button <Button
size="sm" size="sm"
type="submit"
disabled={loading.generate || loading.regenerate || input === ""} disabled={loading.generate || loading.regenerate || input === ""}
onClick={() => handleGenerate({})}
> >
{loading.generate ? ( {loading.generate ? (
<> <>
@ -137,13 +147,14 @@ export default function GenerateInput({
</Button> </Button>
<Button <Button
onClick={onClose} onClick={onClose}
type="button"
variant="outline" variant="outline"
size="smIcon" size="smIcon"
className="bg-transparent shrink-0 border-muted-foreground" className="bg-transparent shrink-0 border-muted-foreground"
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</Button> </Button>
</div> </form>
{expanded ? ( {expanded ? (
<> <>
<div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2"> <div className="rounded-md border border-muted-foreground w-full h-28 overflow-y-scroll p-2">

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({
</> </>
) )
} }

View File

@ -22,6 +22,7 @@ const buttonVariants = cva(
}, },
size: { size: {
default: "h-9 px-4 py-2", default: "h-9 px-4 py-2",
xs: "h-6 px-2.5 py-1.5 rounded-sm text-[0.7rem]",
sm: "h-8 rounded-md px-3 text-xs", sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8", lg: "h-10 rounded-md px-8",
icon: "h-9 w-9", icon: "h-9 w-9",

View File

@ -42,13 +42,13 @@ export default function UserButton({ userData }: { userData: User }) {
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm"> <div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
<div className="flex items-center"> <div className="flex items-center">
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} /> <Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
AI Usage: {userData.generations}/10 AI Usage: {userData.generations}/1000
</div> </div>
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary"> <div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
<div <div
className="h-full bg-indigo-500 rounded-full" className="h-full bg-indigo-500 rounded-full"
style={{ style={{
width: `${(userData.generations * 100) / 10}%`, width: `${(userData.generations * 100) / 1000}%`,
}} }}
/> />
</div> </div>