diff --git a/bun.lockb b/bun.lockb
index 1deb6200..d6a5ad9e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/fake-snippets-api/lib/db/seed.ts b/fake-snippets-api/lib/db/seed.ts
index 4db55a86..1f0c8bd3 100644
--- a/fake-snippets-api/lib/db/seed.ts
+++ b/fake-snippets-api/lib/db/seed.ts
@@ -16,7 +16,7 @@ import { A555Timer } from "@tsci/seveibar.a555timer"
export default () => (
-
+
)`.trim(),
created_at: new Date().toISOString(),
diff --git a/package.json b/package.json
index ffe94bfe..a8956340 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@tscircuit/3d-viewer": "^0.0.32",
+ "@tscircuit/footprinter": "^0.0.68",
"@tscircuit/pcb-viewer": "^1.10.5",
"@types/ms": "^0.7.34",
"@typescript/ata": "^0.9.7",
@@ -88,7 +89,7 @@
"@anthropic-ai/sdk": "^0.27.3",
"@babel/standalone": "^7.25.6",
"@biomejs/biome": "^1.9.2",
- "@tscircuit/core": "^0.0.97",
+ "@tscircuit/core": "^0.0.109",
"@tscircuit/prompt-benchmarks": "^0.0.14",
"@types/babel__standalone": "^7.1.7",
"@types/bun": "^1.1.10",
diff --git a/src/components/CodeAndPreview.tsx b/src/components/CodeAndPreview.tsx
index f32f453a..d4c44c98 100644
--- a/src/components/CodeAndPreview.tsx
+++ b/src/components/CodeAndPreview.tsx
@@ -15,11 +15,12 @@ import { useAxios } from "@/hooks/use-axios"
import { TypeBadge } from "./TypeBadge"
import { useToast } from "@/hooks/use-toast"
import { useMutation, useQueryClient } from "react-query"
-import { ClipboardIcon, Share, Eye, EyeOff } from "lucide-react"
+import { ClipboardIcon, Share, Eye, EyeOff, PlayIcon } from "lucide-react"
import { MagicWandIcon } from "@radix-ui/react-icons"
import { ErrorBoundary } from "react-error-boundary"
import { ErrorTabContent } from "./ErrorTabContent"
import { cn } from "@/lib/utils"
+import { PreviewContent } from "./PreviewContent"
interface Props {
snippet?: Snippet | null
@@ -41,10 +42,16 @@ export function CodeAndPreview({ snippet }: Props) {
}, [snippet?.code])
const { toast } = useToast()
- const { message, circuitJson, compiledJs } = useRunTsx(
+ const {
+ message,
+ circuitJson,
+ compiledJs,
+ triggerRunTsx,
+ tsxRunTriggerCount,
+ } = useRunTsx({
code,
- snippet?.snippet_type,
- )
+ type: snippet?.snippet_type,
+ })
const qc = useQueryClient()
const updateSnippetMutation = useMutation({
@@ -113,43 +120,14 @@ export function CodeAndPreview({ snippet }: Props) {
/>
{showPreview && (
-
-
-
- PCB
- 3D
- JSON
-
- Errors
- {message && (
-
- 1
-
- )}
-
-
-
-
-
-
-
- Error loading 3D viewer
}>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
)}
diff --git a/src/components/ErrorTabContent.tsx b/src/components/ErrorTabContent.tsx
index 3538f478..7b86f260 100644
--- a/src/components/ErrorTabContent.tsx
+++ b/src/components/ErrorTabContent.tsx
@@ -12,7 +12,7 @@ export const ErrorTabContent = ({
}: {
code?: string
isStreaming?: boolean
- errorMessage?: string
+ errorMessage?: string | null
}) => {
const anthropic = useAiApi()
const simplifiedErrorMessage = useAsyncMemo(async () => {
diff --git a/src/components/PreviewContent.tsx b/src/components/PreviewContent.tsx
new file mode 100644
index 00000000..0942ebec
--- /dev/null
+++ b/src/components/PreviewContent.tsx
@@ -0,0 +1,201 @@
+import { useEffect, useMemo, useState } from "react"
+import { CodeEditor } from "@/components/CodeEditor"
+import { PCBViewer } from "@tscircuit/pcb-viewer"
+import { CadViewer } from "@tscircuit/3d-viewer"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { defaultCodeForBlankPage } from "@/lib/defaultCodeForBlankCode"
+import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
+import { encodeTextToUrlHash } from "@/lib/encodeTextToUrlHash"
+import { Button } from "@/components/ui/button"
+import { useRunTsx } from "@/hooks/use-run-tsx"
+import EditorNav from "./EditorNav"
+import { CircuitJsonTableViewer } from "./TableViewer/CircuitJsonTableViewer"
+import { Snippet } from "fake-snippets-api/lib/db/schema"
+import { useAxios } from "@/hooks/use-axios"
+import { TypeBadge } from "./TypeBadge"
+import { useToast } from "@/hooks/use-toast"
+import { useMutation, useQueryClient } from "react-query"
+import { ClipboardIcon, Share, Eye, EyeOff, PlayIcon } from "lucide-react"
+import { MagicWandIcon } from "@radix-ui/react-icons"
+import { ErrorBoundary } from "react-error-boundary"
+import { ErrorTabContent } from "./ErrorTabContent"
+import { cn } from "@/lib/utils"
+import { useCallback } from "react"
+
+export type PreviewContentProps =
+ | {
+ code: string
+ triggerRunTsx: () => void
+ tsxRunTriggerCount: number
+ errorMessage: string | null
+ circuitJson: any
+ className?: string
+ showCodeTab?: false
+ isStreaming?: boolean
+ onCodeChange?: (code: string) => void
+ onDtsChange?: (dts: string) => void
+ }
+ | {
+ code: string
+ triggerRunTsx: () => void
+ tsxRunTriggerCount: number
+ errorMessage: string | null
+ circuitJson: any
+ className?: string
+ showCodeTab: true
+ isStreaming: boolean
+ onCodeChange: (code: string) => void
+ onDtsChange: (dts: string) => void
+ }
+
+const PreviewEmptyState = ({
+ triggerRunTsx,
+}: { triggerRunTsx: () => void }) => (
+
+ No circuit json loaded
+
+
+)
+
+export const PreviewContent = ({
+ code,
+ triggerRunTsx,
+ tsxRunTriggerCount,
+ errorMessage,
+ circuitJson,
+ showCodeTab = false,
+ className,
+ isStreaming,
+ onCodeChange,
+ onDtsChange,
+}: PreviewContentProps) => {
+ const [activeTab, setActiveTab] = useState(showCodeTab ? "code" : "pcb")
+ const [versionOfCodeLastRun, setVersionOfCodeLastRun] = useState("")
+
+ useEffect(() => {
+ if (tsxRunTriggerCount === 0) return
+ setVersionOfCodeLastRun(code)
+ }, [tsxRunTriggerCount])
+
+ useEffect(() => {
+ if (errorMessage) {
+ setActiveTab("error")
+ }
+ }, [errorMessage])
+
+ useEffect(() => {
+ if (activeTab === "code" && circuitJson && !errorMessage) {
+ setActiveTab("pcb")
+ }
+ }, [circuitJson])
+
+ return (
+
+
+
+
+
+
+ {showCodeTab && Code}
+
+ {circuitJson && (
+
+ )}
+ PCB
+
+
+ {circuitJson && (
+
+ )}
+ 3D
+
+ JSON
+
+ Errors
+ {errorMessage && (
+
+ 1
+
+ )}
+
+
+
+ {showCodeTab && (
+
+
+
+ )}
+
+
+ Error loading PCB viewer
}>
+ {circuitJson ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Error loading 3D viewer
}>
+ {circuitJson ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Error loading 3D viewer
}>
+ {circuitJson ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {circuitJson || errorMessage ? (
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/src/hooks/use-compiled-tsx.ts b/src/hooks/use-compiled-tsx.ts
index 6e20611c..d56b62b8 100644
--- a/src/hooks/use-compiled-tsx.ts
+++ b/src/hooks/use-compiled-tsx.ts
@@ -4,8 +4,8 @@ import * as Babel from "@babel/standalone"
export const safeCompileTsx = (
code: string,
):
- | { success: true; compiledTsx: string }
- | { success: false; error: Error } => {
+ | { success: true; compiledTsx: string; error?: undefined }
+ | { success: false; error: Error; compiledTsx?: undefined } => {
try {
return {
success: true,
diff --git a/src/hooks/use-run-tsx/construct-circuit.tsx b/src/hooks/use-run-tsx/construct-circuit.tsx
index c291465f..60c66dfd 100644
--- a/src/hooks/use-run-tsx/construct-circuit.tsx
+++ b/src/hooks/use-run-tsx/construct-circuit.tsx
@@ -4,7 +4,6 @@ import * as React from "react"
import { useCompiledTsx } from "../use-compiled-tsx"
import { createJSCADRenderer } from "jscad-fiber"
import { jscadPlanner } from "jscad-planner"
-import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-utils"
export const constructCircuit = (
UserElm: any,
diff --git a/src/hooks/use-run-tsx/index.tsx b/src/hooks/use-run-tsx/index.tsx
index 77552e19..e9721cdc 100644
--- a/src/hooks/use-run-tsx/index.tsx
+++ b/src/hooks/use-run-tsx/index.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useMemo, useState } from "react"
+import { useEffect, useMemo, useReducer, useState } from "react"
import * as React from "react"
-import { useCompiledTsx } from "../use-compiled-tsx"
+import { safeCompileTsx, useCompiledTsx } from "../use-compiled-tsx"
import { Circuit } from "@tscircuit/core"
import { createJSCADRenderer } from "jscad-fiber"
import { jscadPlanner } from "jscad-planner"
@@ -17,13 +17,23 @@ type RunTsxResult = {
isLoading: boolean
}
-export const useRunTsx = (
- code?: string,
- type?: "board" | "footprint" | "package" | "model",
- { isStreaming = false }: { isStreaming?: boolean } = {},
-): RunTsxResult => {
+export const useRunTsx = ({
+ code,
+ type,
+ isStreaming = false,
+}: {
+ code?: string
+ type?: "board" | "footprint" | "package" | "model"
+ isStreaming?: boolean
+} = {}): RunTsxResult & {
+ triggerRunTsx: () => void
+ tsxRunTriggerCount: number
+} => {
type ??= "board"
- const compiledJs = useCompiledTsx(code, { isStreaming })
+ const [tsxRunTriggerCount, incTsxRunTriggerCount] = useReducer(
+ (c) => c + 1,
+ 0,
+ )
const [tsxResult, setTsxResult] = useState({
compiledModule: null,
message: "",
@@ -33,15 +43,26 @@ export const useRunTsx = (
const apiBaseUrl = useSnippetsBaseApiUrl()
useEffect(() => {
+ if (tsxRunTriggerCount === 0) return
+ if (isStreaming) {
+ setTsxResult({
+ compiledModule: null,
+ message: "",
+ circuitJson: null,
+ isLoading: false,
+ })
+ }
+ if (!code) return
async function run() {
- if (isStreaming || !compiledJs || !code) {
+ const { success, compiledTsx: compiledJs, error } = safeCompileTsx(code!)
+
+ if (!success) {
setTsxResult({
compiledModule: null,
- message: "",
+ message: `Compile Error: ${error.message}`,
circuitJson: null,
isLoading: false,
})
- return
}
const imports = getImportsFromCode(code!).filter((imp) =>
@@ -54,18 +75,15 @@ export const useRunTsx = (
const fullSnippetName = importName
.replace("@tsci/", "")
.replace(".", "/")
- console.log({ importName, fullSnippetName })
// Fetch compiled code from the server
const { snippet: importedSnippet } = await fetch(
`${apiBaseUrl}/snippets/get?name=${fullSnippetName}`,
).then((res) => res.json())
try {
- console.log("importedSnippet", importedSnippet)
- // eval the imported snippet compiled_js
preSuppliedImports[importName] = evalCompiledJs(
importedSnippet.compiled_js,
- )
+ ).exports
} catch (e) {
console.error("Error importing snippet", e)
}
@@ -73,7 +91,9 @@ export const useRunTsx = (
const __tscircuit_require = (name: string) => {
if (!preSuppliedImports[name]) {
- throw new Error(`Import "${name}" not found`)
+ throw new Error(
+ `Import "${name}" not found (imports available: ${Object.keys(preSuppliedImports).join(",")})`,
+ )
}
return preSuppliedImports[name]
}
@@ -82,7 +102,7 @@ export const useRunTsx = (
try {
globalThis.React = React
- const module = evalCompiledJs(compiledJs)
+ const module = evalCompiledJs(compiledJs!)
if (Object.keys(module.exports).length > 1) {
throw new Error(
@@ -127,7 +147,11 @@ export const useRunTsx = (
}
}
run()
- }, [compiledJs, isStreaming])
+ }, [tsxRunTriggerCount])
- return tsxResult
+ return {
+ ...tsxResult,
+ triggerRunTsx: incTsxRunTriggerCount,
+ tsxRunTriggerCount,
+ }
}
diff --git a/src/pages/ai.tsx b/src/pages/ai.tsx
index 8d4d340a..11f2e3b8 100644
--- a/src/pages/ai.tsx
+++ b/src/pages/ai.tsx
@@ -14,12 +14,20 @@ import { useLocation } from "wouter"
import { useSaveSnippet } from "@/hooks/use-save-snippet"
import { useToast } from "@/hooks/use-toast"
import { useSnippet } from "@/hooks/use-snippet"
+import { PreviewContent } from "@/components/PreviewContent"
export const AiPage = () => {
const [code, setCode] = useState("")
const [dts, setDts] = useState("")
const [isStreaming, setIsStreaming] = useState(false)
- const { message: errorMessage, circuitJson } = useRunTsx(code, "board", {
+ const {
+ message: errorMessage,
+ circuitJson,
+ triggerRunTsx,
+ tsxRunTriggerCount,
+ } = useRunTsx({
+ code,
+ type: "board",
isStreaming,
})
const { saveSnippet, isLoading: isSaving } = useSaveSnippet()
@@ -60,87 +68,18 @@ export const AiPage = () => {
-
-
-
-
- Code
- PCB
- 3D
-
- Errors
- {errorMessage && (
-
- 1
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {circuitJson ? (
-
- ) : (
- "No Circuit JSON (might be an error in the snippet)"
- )}
-
-
-
-
- {circuitJson ? (
-
- ) : (
- "No Circuit JSON (might be an error in the snippet)"
- )}
-
-
-
-
-
-
-
+
diff --git a/src/pages/view-snippet.tsx b/src/pages/view-snippet.tsx
index 4178920a..d8db8258 100644
--- a/src/pages/view-snippet.tsx
+++ b/src/pages/view-snippet.tsx
@@ -18,10 +18,10 @@ export const ViewSnippetPage = () => {
const { author, snippetName } = useParams()
const { snippet } = useCurrentSnippet()
- const { circuitJson, message } = useRunTsx(
- snippet?.code ?? "",
- snippet?.snippet_type,
- )
+ const { circuitJson, message } = useRunTsx({
+ code: snippet?.code ?? "",
+ type: snippet?.snippet_type,
+ })
return (