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 (