@tscircuit/fake-snippets 0.0.66 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
- package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
- package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
- package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
- package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
- package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
- package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +0 -11
- package/bun.lock +15 -17
- package/dist/bundle.js +32 -39
- package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
- package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
- package/package.json +4 -3
- package/src/App.tsx +0 -7
- package/src/ContextProviders.tsx +2 -0
- package/src/components/DownloadButtonAndMenu.tsx +1 -4
- package/src/components/Footer.tsx +5 -2
- package/src/components/HeaderLogin.tsx +37 -54
- package/src/components/ImageWithFallback.tsx +37 -0
- package/src/components/JLCPCBImportDialog.tsx +43 -24
- package/src/components/PackageCard.tsx +2 -2
- package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
- package/src/components/PackageSearchResults.tsx +87 -0
- package/src/components/PackagesList.tsx +3 -3
- package/src/components/PageSearchComponent.tsx +9 -9
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
- package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -8
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +24 -14
- package/src/components/ViewPackagePage/components/package-header.tsx +6 -1
- package/src/components/package-port/CodeEditor.tsx +13 -10
- package/src/components/package-port/CodeEditorHeader.tsx +1 -1
- package/src/components/package-port/EditorNav.tsx +2 -2
- package/src/hooks/use-global-store.ts +1 -0
- package/src/hooks/use-shiki-highlighter.ts +13 -6
- package/src/lib/download-fns/download-gltf.ts +3 -10
- package/src/lib/handleManualEditsImport.tsx +1 -1
- package/src/lib/types.ts +4 -2
- package/src/pages/dashboard.tsx +3 -4
- package/src/pages/editor.tsx +20 -14
- package/src/pages/latest.tsx +25 -26
- package/src/pages/search.tsx +120 -19
- package/src/pages/trending.tsx +14 -58
- package/src/pages/user-profile.tsx +13 -8
- package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
- package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
- package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
- package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
- package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
- package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
- package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
- package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
- package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
- package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
- package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
- package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
- package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
- package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
- package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
- package/src/components/AiChatInterface.tsx +0 -229
- package/src/components/CodeAndPreview.tsx +0 -289
- package/src/components/CodeEditor.tsx +0 -539
- package/src/components/CodeEditorHeader.tsx +0 -135
- package/src/components/EditorNav.tsx +0 -502
- package/src/components/PreviewContent.tsx +0 -372
- package/src/components/SnippetCard.tsx +0 -159
- package/src/components/SnippetList.tsx +0 -71
- package/src/hooks/use-compiled-tsx.ts +0 -37
- package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
- package/src/hooks/use-run-tsx/index.tsx +0 -256
- package/src/hooks/use-save-snippet.ts +0 -66
- package/src/hooks/use-typecheck.ts +0 -54
- package/src/lib/utils/getSyntaxError.ts +0 -13
- package/src/pages/ai.tsx +0 -92
- package/src/pages/view-snippet.tsx +0 -166
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from "react"
|
|
2
|
-
import { Button } from "@/components/ui/button"
|
|
3
|
-
import ChatInput from "./ChatInput"
|
|
4
|
-
import { useAiApi } from "@/hooks/use-ai-api"
|
|
5
|
-
import { createCircuitBoard1Template } from "@tscircuit/prompt-benchmarks"
|
|
6
|
-
import { TextDelta } from "@anthropic-ai/sdk/resources/messages.mjs"
|
|
7
|
-
import { MagicWandIcon } from "@radix-ui/react-icons"
|
|
8
|
-
import { AiChatMessage } from "./AiChatMessage"
|
|
9
|
-
import { useLocation } from "wouter"
|
|
10
|
-
import { useSnippet } from "@/hooks/use-snippet"
|
|
11
|
-
import { Edit2 } from "lucide-react"
|
|
12
|
-
import { SnippetLink } from "./SnippetLink"
|
|
13
|
-
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
14
|
-
import { useSignIn } from "@/hooks/use-sign-in"
|
|
15
|
-
import { extractCodefence } from "extract-codefence"
|
|
16
|
-
import { PrefetchPageLink } from "./PrefetchPageLink"
|
|
17
|
-
|
|
18
|
-
export default function AIChatInterface({
|
|
19
|
-
code,
|
|
20
|
-
hasUnsavedChanges,
|
|
21
|
-
snippetId,
|
|
22
|
-
onCodeChange,
|
|
23
|
-
onStartStreaming,
|
|
24
|
-
onStopStreaming,
|
|
25
|
-
errorMessage,
|
|
26
|
-
disabled,
|
|
27
|
-
}: {
|
|
28
|
-
code: string
|
|
29
|
-
disabled?: boolean
|
|
30
|
-
hasUnsavedChanges: boolean
|
|
31
|
-
snippetId?: string | null
|
|
32
|
-
onCodeChange: (code: string) => void
|
|
33
|
-
onStartStreaming: () => void
|
|
34
|
-
onStopStreaming: () => void
|
|
35
|
-
errorMessage: string | null
|
|
36
|
-
}) {
|
|
37
|
-
const [messages, setMessages] = useState<AiChatMessage[]>([])
|
|
38
|
-
const [isStreaming, setIsStreaming] = useState(false)
|
|
39
|
-
const anthropic = useAiApi()
|
|
40
|
-
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
41
|
-
const { data: snippet } = useSnippet(snippetId!)
|
|
42
|
-
const [currentCodeBlock, setCurrentCodeBlock] = useState<string | null>(null)
|
|
43
|
-
const [location, navigate] = useLocation()
|
|
44
|
-
const isStreamingRef = useRef(false)
|
|
45
|
-
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
|
|
46
|
-
const signIn = useSignIn()
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
|
50
|
-
}, [messages])
|
|
51
|
-
|
|
52
|
-
const addMessage = async (message: string) => {
|
|
53
|
-
const newMessages = messages.concat([
|
|
54
|
-
{
|
|
55
|
-
sender: "user",
|
|
56
|
-
content: message,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
sender: "bot",
|
|
60
|
-
content: "",
|
|
61
|
-
codeVersion: messages.filter((m) => m.sender === "bot").length,
|
|
62
|
-
},
|
|
63
|
-
])
|
|
64
|
-
setMessages(newMessages)
|
|
65
|
-
setIsStreaming(true)
|
|
66
|
-
onStartStreaming()
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const stream = await anthropic.messages.stream({
|
|
70
|
-
model: "claude-3-sonnet-20240229",
|
|
71
|
-
system: createCircuitBoard1Template({
|
|
72
|
-
currentCode: code,
|
|
73
|
-
}),
|
|
74
|
-
messages: [
|
|
75
|
-
// TODO: include previous messages
|
|
76
|
-
{
|
|
77
|
-
role: "user",
|
|
78
|
-
content: message,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
max_tokens: 1000,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
let accumulatedContent = ""
|
|
85
|
-
let isInCodeBlock = false
|
|
86
|
-
|
|
87
|
-
for await (const chunk of stream) {
|
|
88
|
-
if (chunk.type === "content_block_delta") {
|
|
89
|
-
const chunkText = (chunk.delta as TextDelta).text
|
|
90
|
-
accumulatedContent += chunkText
|
|
91
|
-
|
|
92
|
-
if (chunkText.includes("```")) {
|
|
93
|
-
isInCodeBlock = !isInCodeBlock
|
|
94
|
-
if (isInCodeBlock) {
|
|
95
|
-
setCurrentCodeBlock("")
|
|
96
|
-
} else {
|
|
97
|
-
const codeContent = extractCodefence(accumulatedContent)
|
|
98
|
-
if (codeContent) {
|
|
99
|
-
onCodeChange(codeContent)
|
|
100
|
-
}
|
|
101
|
-
setCurrentCodeBlock(null)
|
|
102
|
-
}
|
|
103
|
-
} else if (isInCodeBlock) {
|
|
104
|
-
setCurrentCodeBlock((prev) => {
|
|
105
|
-
const updatedCode = (prev || "") + chunkText
|
|
106
|
-
onCodeChange(updatedCode)
|
|
107
|
-
return updatedCode
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
setMessages((prevMessages) => {
|
|
112
|
-
const updatedMessages = [...prevMessages]
|
|
113
|
-
updatedMessages[updatedMessages.length - 1].content =
|
|
114
|
-
accumulatedContent
|
|
115
|
-
return updatedMessages
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error("Error streaming response:", error)
|
|
121
|
-
setMessages((prevMessages) => {
|
|
122
|
-
const updatedMessages = [...prevMessages]
|
|
123
|
-
updatedMessages[updatedMessages.length - 1].content =
|
|
124
|
-
"An error occurred while generating the response."
|
|
125
|
-
return updatedMessages
|
|
126
|
-
})
|
|
127
|
-
} finally {
|
|
128
|
-
setIsStreaming(false)
|
|
129
|
-
onStopStreaming()
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
const searchParams = new URLSearchParams(
|
|
135
|
-
window.location.search.split("?")[1],
|
|
136
|
-
)
|
|
137
|
-
const initialPrompt = searchParams.get("initial_prompt")
|
|
138
|
-
|
|
139
|
-
if (initialPrompt && messages.length === 0 && !isStreamingRef.current) {
|
|
140
|
-
isStreamingRef.current = true
|
|
141
|
-
addMessage(initialPrompt)
|
|
142
|
-
}
|
|
143
|
-
}, [])
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<div className="flex flex-col h-[calc(100vh-60px)] max-w-2xl mx-auto p-4 bg-gray-100">
|
|
147
|
-
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
|
|
148
|
-
{snippet && (
|
|
149
|
-
<div className="flex pl-4 p-2 rounded items-center bg-white border border-gray-200 text-sm mb-4 shadow-sm">
|
|
150
|
-
<SnippetLink snippet={snippet} />
|
|
151
|
-
<div className="flex-grow" />
|
|
152
|
-
<PrefetchPageLink href={`/editor?snippet_id=${snippet.snippet_id}`}>
|
|
153
|
-
<Button
|
|
154
|
-
size="sm"
|
|
155
|
-
className="text-xs"
|
|
156
|
-
variant="ghost"
|
|
157
|
-
disabled={hasUnsavedChanges}
|
|
158
|
-
>
|
|
159
|
-
Open in Editor
|
|
160
|
-
<Edit2 className="w-3 h-3 ml-2 opacity-60" />
|
|
161
|
-
</Button>
|
|
162
|
-
</PrefetchPageLink>
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
{messages.length === 0 && isLoggedIn && (
|
|
166
|
-
<div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
|
|
167
|
-
<div>Submit a prompt to {snippet ? "edit!" : "get started!"}</div>
|
|
168
|
-
<div className="mt-2">
|
|
169
|
-
This is our legacy AI chat interface. For a better experience,
|
|
170
|
-
please use{" "}
|
|
171
|
-
<PrefetchPageLink href="https://chat.tscircuit.com">
|
|
172
|
-
chat.tscircuit.com
|
|
173
|
-
</PrefetchPageLink>
|
|
174
|
-
.
|
|
175
|
-
</div>
|
|
176
|
-
<div className="text-6xl mt-4">↓</div>
|
|
177
|
-
</div>
|
|
178
|
-
)}
|
|
179
|
-
{!isLoggedIn && (
|
|
180
|
-
<div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
|
|
181
|
-
<div>
|
|
182
|
-
Sign in use the AI chat or{" "}
|
|
183
|
-
<PrefetchPageLink
|
|
184
|
-
className="text-blue-500 underline"
|
|
185
|
-
href="/quickstart"
|
|
186
|
-
>
|
|
187
|
-
use the regular editor
|
|
188
|
-
</PrefetchPageLink>
|
|
189
|
-
</div>
|
|
190
|
-
<div className="mt-4 flex gap-2">
|
|
191
|
-
<Button onClick={() => signIn()}>Sign In</Button>
|
|
192
|
-
<Button onClick={() => signIn()} variant="outline">
|
|
193
|
-
Sign Up
|
|
194
|
-
</Button>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
)}
|
|
198
|
-
{messages.map((message, index) => (
|
|
199
|
-
<AiChatMessage key={index} message={message} />
|
|
200
|
-
))}
|
|
201
|
-
<div ref={messagesEndRef} />
|
|
202
|
-
</div>
|
|
203
|
-
{code && errorMessage && !isStreaming && (
|
|
204
|
-
<div className="flex justify-end mr-6">
|
|
205
|
-
<Button
|
|
206
|
-
onClick={() => {
|
|
207
|
-
addMessage(`Fix this error: ${errorMessage}`)
|
|
208
|
-
}}
|
|
209
|
-
disabled={!isLoggedIn}
|
|
210
|
-
className="mb-2 bg-green-50 hover:bg-green-100"
|
|
211
|
-
variant="outline"
|
|
212
|
-
>
|
|
213
|
-
<MagicWandIcon className="w-4 h-4 mr-2" />
|
|
214
|
-
<span className="font-bold">Fix Error with AI</span>
|
|
215
|
-
<span className="italic font-normal ml-2">
|
|
216
|
-
"{errorMessage.slice(0, 26)}..."
|
|
217
|
-
</span>
|
|
218
|
-
</Button>
|
|
219
|
-
</div>
|
|
220
|
-
)}
|
|
221
|
-
<ChatInput
|
|
222
|
-
onSubmit={async (message: string) => {
|
|
223
|
-
addMessage(message)
|
|
224
|
-
}}
|
|
225
|
-
disabled={isStreaming || !isLoggedIn}
|
|
226
|
-
/>
|
|
227
|
-
</div>
|
|
228
|
-
)
|
|
229
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import { CodeEditor } from "@/components/CodeEditor"
|
|
2
|
-
import { usePackageVisibilitySettingsDialog } from "@/components/dialogs/package-visibility-settings-dialog"
|
|
3
|
-
import { useAxios } from "@/hooks/use-axios"
|
|
4
|
-
import { useCreateSnippetMutation } from "@/hooks/use-create-snippet-mutation"
|
|
5
|
-
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
6
|
-
import { useToast } from "@/hooks/use-toast"
|
|
7
|
-
import { useUrlParams } from "@/hooks/use-url-params"
|
|
8
|
-
import useWarnUserOnPageChange from "@/hooks/use-warn-user-on-page-change"
|
|
9
|
-
import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
|
|
10
|
-
import { getSnippetTemplate } from "@/lib/get-snippet-template"
|
|
11
|
-
import { cn } from "@/lib/utils"
|
|
12
|
-
import { parseJsonOrNull } from "@/lib/utils/parseJsonOrNull"
|
|
13
|
-
import type { Snippet } from "fake-snippets-api/lib/db/schema"
|
|
14
|
-
import { Loader2 } from "lucide-react"
|
|
15
|
-
import { useEffect, useMemo, useState } from "react"
|
|
16
|
-
import { useMutation, useQueryClient } from "react-query"
|
|
17
|
-
import EditorNav from "./EditorNav"
|
|
18
|
-
import { SuspenseRunFrame } from "./SuspenseRunFrame"
|
|
19
|
-
import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
|
|
20
|
-
|
|
21
|
-
interface Props {
|
|
22
|
-
snippet?: Snippet | null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function CodeAndPreview({ snippet }: Props) {
|
|
26
|
-
const axios = useAxios()
|
|
27
|
-
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
|
|
28
|
-
const urlParams = useUrlParams()
|
|
29
|
-
const templateFromUrl = useMemo(
|
|
30
|
-
() => (urlParams.template ? getSnippetTemplate(urlParams.template) : null),
|
|
31
|
-
[],
|
|
32
|
-
)
|
|
33
|
-
const defaultCode = useMemo(() => {
|
|
34
|
-
return (
|
|
35
|
-
decodeUrlHashToText(window.location.toString()) ??
|
|
36
|
-
snippet?.code ??
|
|
37
|
-
// If the snippet_id is in the url, use an empty string as the default
|
|
38
|
-
// code until the snippet code is loaded
|
|
39
|
-
(urlParams.snippet_id && "") ??
|
|
40
|
-
templateFromUrl?.code
|
|
41
|
-
)
|
|
42
|
-
}, [])
|
|
43
|
-
|
|
44
|
-
// Initialize with template or snippet's manual edits if available
|
|
45
|
-
const [manualEditsFileContent, setManualEditsFileContent] = useState<
|
|
46
|
-
string | null
|
|
47
|
-
>(null)
|
|
48
|
-
const [code, setCode] = useState(defaultCode ?? "")
|
|
49
|
-
const [dts, setDts] = useState("")
|
|
50
|
-
const [showPreview, setShowPreview] = useState(true)
|
|
51
|
-
const [lastRunCode, setLastRunCode] = useState(defaultCode ?? "")
|
|
52
|
-
const [fullScreen, setFullScreen] = useState(false)
|
|
53
|
-
const [circuitJson, setCircuitJson] = useState<any>(null)
|
|
54
|
-
const {
|
|
55
|
-
Dialog: PackageVisibilitySettingsDialog,
|
|
56
|
-
openDialog: openPackageVisibilitySettingsDialog,
|
|
57
|
-
} = usePackageVisibilitySettingsDialog()
|
|
58
|
-
const [isPrivate, setIsPrivate] = useState(false)
|
|
59
|
-
|
|
60
|
-
const snippetType: "board" | "package" | "model" | "footprint" =
|
|
61
|
-
snippet?.snippet_type ??
|
|
62
|
-
(templateFromUrl?.type as any) ??
|
|
63
|
-
urlParams.snippet_type
|
|
64
|
-
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (snippet?.code) {
|
|
67
|
-
setCode(snippet.code)
|
|
68
|
-
setLastRunCode(snippet.code)
|
|
69
|
-
}
|
|
70
|
-
}, [Boolean(snippet)])
|
|
71
|
-
|
|
72
|
-
const { toast } = useToast()
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (snippet?.manual_edits_json_content) {
|
|
76
|
-
setManualEditsFileContent(snippet.manual_edits_json_content ?? "")
|
|
77
|
-
}
|
|
78
|
-
}, [Boolean(snippet?.manual_edits_json_content)])
|
|
79
|
-
|
|
80
|
-
const userImports = useMemo(
|
|
81
|
-
() => ({
|
|
82
|
-
"./manual-edits.json": parseJsonOrNull(manualEditsFileContent) ?? "",
|
|
83
|
-
}),
|
|
84
|
-
[manualEditsFileContent],
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
const qc = useQueryClient()
|
|
88
|
-
|
|
89
|
-
const updateSnippetMutation = useMutation({
|
|
90
|
-
mutationFn: async () => {
|
|
91
|
-
if (!snippet) throw new Error("No snippet to update")
|
|
92
|
-
|
|
93
|
-
const updateSnippetPayload = {
|
|
94
|
-
snippet_id: snippet.snippet_id,
|
|
95
|
-
code: code,
|
|
96
|
-
dts: dts,
|
|
97
|
-
// compiled_js: compiledJs,
|
|
98
|
-
circuit_json: circuitJson,
|
|
99
|
-
manual_edits_json_content: manualEditsFileContent,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const response = await axios.post(
|
|
104
|
-
"/snippets/update",
|
|
105
|
-
updateSnippetPayload,
|
|
106
|
-
)
|
|
107
|
-
return response.data
|
|
108
|
-
} catch (error: any) {
|
|
109
|
-
const responseStatus = error?.status ?? error?.response?.status
|
|
110
|
-
// We would normally only do this if the error is a 413, but we're not
|
|
111
|
-
// able to check the status properly because of the browser CORS policy
|
|
112
|
-
// (the PAYLOAD_TOO_LARGE error does not have the proper CORS headers)
|
|
113
|
-
if (
|
|
114
|
-
import.meta.env.VITE_ALTERNATE_REGISTRY_URL &&
|
|
115
|
-
(responseStatus === undefined || responseStatus === 413)
|
|
116
|
-
) {
|
|
117
|
-
console.log(`Failed to update snippet, attempting alternate registry`)
|
|
118
|
-
const response = await axios.post(
|
|
119
|
-
`${import.meta.env.VITE_ALTERNATE_REGISTRY_URL}/snippets/update`,
|
|
120
|
-
updateSnippetPayload,
|
|
121
|
-
)
|
|
122
|
-
return response.data
|
|
123
|
-
}
|
|
124
|
-
throw error
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
onSuccess: () => {
|
|
128
|
-
qc.invalidateQueries({ queryKey: ["snippets", snippet?.snippet_id] })
|
|
129
|
-
toast({
|
|
130
|
-
title: "Snippet saved",
|
|
131
|
-
description: "Your changes have been saved successfully.",
|
|
132
|
-
})
|
|
133
|
-
},
|
|
134
|
-
onError: (error) => {
|
|
135
|
-
console.error("Error saving snippet:", error)
|
|
136
|
-
toast({
|
|
137
|
-
title: "Error",
|
|
138
|
-
description:
|
|
139
|
-
error instanceof Error
|
|
140
|
-
? error.message
|
|
141
|
-
: "Failed to save the snippet. Please try again.",
|
|
142
|
-
variant: "destructive",
|
|
143
|
-
})
|
|
144
|
-
},
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const createSnippetMutation = useCreateSnippetMutation()
|
|
148
|
-
const [lastSavedAt, setLastSavedAt] = useState(Date.now())
|
|
149
|
-
|
|
150
|
-
const handleSave = async () => {
|
|
151
|
-
if (hasUnrunChanges) {
|
|
152
|
-
toast({
|
|
153
|
-
title: "Warning",
|
|
154
|
-
description: "You must run the snippet before saving your changes.",
|
|
155
|
-
variant: "destructive",
|
|
156
|
-
})
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!snippet && isLoggedIn) {
|
|
161
|
-
openPackageVisibilitySettingsDialog()
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
setLastSavedAt(Date.now())
|
|
166
|
-
if (snippet) {
|
|
167
|
-
updateSnippetMutation.mutate()
|
|
168
|
-
} else {
|
|
169
|
-
createSnippetMutation.mutate({
|
|
170
|
-
code,
|
|
171
|
-
circuit_json: circuitJson as any,
|
|
172
|
-
manual_edits_json_content: manualEditsFileContent ?? "",
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const hasManualEditsChanged =
|
|
178
|
-
(snippet?.manual_edits_json_content ?? "") !==
|
|
179
|
-
(manualEditsFileContent ?? "")
|
|
180
|
-
|
|
181
|
-
const hasUnsavedChanges =
|
|
182
|
-
!updateSnippetMutation.isLoading &&
|
|
183
|
-
Date.now() - lastSavedAt > 1000 &&
|
|
184
|
-
(snippet?.code !== code || hasManualEditsChanged)
|
|
185
|
-
|
|
186
|
-
const hasUnrunChanges = code !== lastRunCode
|
|
187
|
-
|
|
188
|
-
useWarnUserOnPageChange({ hasUnsavedChanges })
|
|
189
|
-
|
|
190
|
-
const fsMap = useMemo(() => {
|
|
191
|
-
return {
|
|
192
|
-
"index.tsx": code,
|
|
193
|
-
"manual-edits.json": manualEditsFileContent || "",
|
|
194
|
-
}
|
|
195
|
-
}, [code, manualEditsFileContent])
|
|
196
|
-
|
|
197
|
-
if (!snippet && (urlParams.snippet_id || urlParams.should_create_snippet)) {
|
|
198
|
-
return (
|
|
199
|
-
<div className="flex items-center justify-center h-64">
|
|
200
|
-
<div className="flex flex-col items-center justify-center">
|
|
201
|
-
<div className="text-lg text-gray-500 mb-4">Loading</div>
|
|
202
|
-
<Loader2 className="w-16 h-16 animate-spin text-gray-400" />
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return (
|
|
209
|
-
<div className="flex flex-col">
|
|
210
|
-
<EditorNav
|
|
211
|
-
circuitJson={circuitJson}
|
|
212
|
-
snippet={snippet}
|
|
213
|
-
snippetType={snippetType}
|
|
214
|
-
code={code}
|
|
215
|
-
isSaving={updateSnippetMutation.isLoading}
|
|
216
|
-
hasUnsavedChanges={hasUnsavedChanges}
|
|
217
|
-
onSave={() => handleSave()}
|
|
218
|
-
onTogglePreview={() => setShowPreview(!showPreview)}
|
|
219
|
-
previewOpen={showPreview}
|
|
220
|
-
canSave={!hasUnrunChanges} // Disable save if there are unrun changes
|
|
221
|
-
/>
|
|
222
|
-
<div className={`flex ${showPreview ? "flex-col md:flex-row" : ""}`}>
|
|
223
|
-
<div
|
|
224
|
-
className={cn(
|
|
225
|
-
"hidden flex-col md:flex border-r border-gray-200 bg-gray-50",
|
|
226
|
-
showPreview ? "w-full md:w-1/2" : "w-full flex",
|
|
227
|
-
)}
|
|
228
|
-
>
|
|
229
|
-
<CodeEditor
|
|
230
|
-
initialCode={code}
|
|
231
|
-
manualEditsFileContent={manualEditsFileContent ?? ""}
|
|
232
|
-
onManualEditsFileContentChanged={(newContent) => {
|
|
233
|
-
setManualEditsFileContent(newContent)
|
|
234
|
-
}}
|
|
235
|
-
onCodeChange={(newCode) => {
|
|
236
|
-
setCode(newCode)
|
|
237
|
-
}}
|
|
238
|
-
onDtsChange={(newDts) => setDts(newDts)}
|
|
239
|
-
/>
|
|
240
|
-
</div>
|
|
241
|
-
{showPreview && (
|
|
242
|
-
<div
|
|
243
|
-
className={cn(
|
|
244
|
-
"flex p-0 flex-col min-h-[640px]",
|
|
245
|
-
fullScreen
|
|
246
|
-
? "fixed inset-0 z-50 bg-white p-4 overflow-hidden"
|
|
247
|
-
: "w-full md:w-1/2",
|
|
248
|
-
)}
|
|
249
|
-
>
|
|
250
|
-
<SuspenseRunFrame
|
|
251
|
-
showRunButton
|
|
252
|
-
forceLatestEvalVersion
|
|
253
|
-
onRenderStarted={() => {
|
|
254
|
-
setLastRunCode(code)
|
|
255
|
-
}}
|
|
256
|
-
onRenderFinished={({ circuitJson }) => {
|
|
257
|
-
setCircuitJson(circuitJson)
|
|
258
|
-
}}
|
|
259
|
-
onEditEvent={(event) => {
|
|
260
|
-
const newManualEditsFileContent =
|
|
261
|
-
applyEditEventsToManualEditsFile({
|
|
262
|
-
circuitJson: circuitJson,
|
|
263
|
-
editEvents: [event],
|
|
264
|
-
manualEditsFile: JSON.parse(manualEditsFileContent ?? "{}"),
|
|
265
|
-
})
|
|
266
|
-
setManualEditsFileContent(
|
|
267
|
-
JSON.stringify(newManualEditsFileContent, null, 2),
|
|
268
|
-
)
|
|
269
|
-
}}
|
|
270
|
-
fsMap={fsMap}
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
)}
|
|
274
|
-
</div>
|
|
275
|
-
<PackageVisibilitySettingsDialog
|
|
276
|
-
initialIsPrivate={false}
|
|
277
|
-
onSave={(isPrivate: boolean) => {
|
|
278
|
-
setLastSavedAt(Date.now())
|
|
279
|
-
createSnippetMutation.mutate({
|
|
280
|
-
code,
|
|
281
|
-
circuit_json: circuitJson as any,
|
|
282
|
-
manual_edits_json_content: manualEditsFileContent ?? "",
|
|
283
|
-
is_private: isPrivate,
|
|
284
|
-
})
|
|
285
|
-
}}
|
|
286
|
-
/>
|
|
287
|
-
</div>
|
|
288
|
-
)
|
|
289
|
-
}
|