@tscircuit/runframe 0.0.1

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.
Files changed (50) hide show
  1. package/.github/workflows/bun-formatcheck.yml +26 -0
  2. package/.github/workflows/bun-pver-release.yml +25 -0
  3. package/.github/workflows/bun-typecheck.yml +26 -0
  4. package/LICENSE +21 -0
  5. package/README.md +40 -0
  6. package/biome.json +57 -0
  7. package/bun.lockb +0 -0
  8. package/cosmos.config.json +3 -0
  9. package/cosmos.decorator.tsx +3 -0
  10. package/dist/assets/index-CNL2S1zq.js +36 -0
  11. package/dist/assets/index-DDl5GwFA.js +21609 -0
  12. package/dist/assets/vision_bundle-CPRQl8Yc.js +15 -0
  13. package/dist/index.html +13 -0
  14. package/dist/standalone.js +46887 -0
  15. package/examples/resistor.fixture.tsx +12 -0
  16. package/examples/runframe-with-api-1.fixture.tsx +41 -0
  17. package/examples/runframe1-basic.fixture.tsx +19 -0
  18. package/index.html +13 -0
  19. package/lib/components/BomTable.tsx +69 -0
  20. package/lib/components/CircuitJsonPreview.tsx +348 -0
  21. package/lib/components/CircuitJsonTableViewer/CircuitJsonTableViewer.tsx +315 -0
  22. package/lib/components/CircuitJsonTableViewer/ClickableText.tsx +20 -0
  23. package/lib/components/CircuitJsonTableViewer/HeaderCell.tsx +26 -0
  24. package/lib/components/CircuitJsonTableViewer/Modal.tsx +38 -0
  25. package/lib/components/ErrorFallback.tsx +25 -0
  26. package/lib/components/ErrorTabContent.tsx +86 -0
  27. package/lib/components/PcbViewerWithContainerHeight.tsx +47 -0
  28. package/lib/components/PreviewEmptyState.tsx +14 -0
  29. package/lib/components/RunFrame.tsx +68 -0
  30. package/lib/components/RunFrameWithApi/index.tsx +41 -0
  31. package/lib/components/RunFrameWithApi/standalone.tsx +6 -0
  32. package/lib/components/RunFrameWithApi/store.ts +122 -0
  33. package/lib/components/RunFrameWithApi/types.ts +43 -0
  34. package/lib/components/ui/button.tsx +57 -0
  35. package/lib/components/ui/dropdown-menu.tsx +203 -0
  36. package/lib/components/ui/tabs.tsx +53 -0
  37. package/lib/dev/render-to-circuit-json.ts +7 -0
  38. package/lib/hooks/use-styles.ts +17 -0
  39. package/lib/preview.ts +1 -0
  40. package/lib/runner.ts +2 -0
  41. package/lib/utils/index.ts +6 -0
  42. package/lib/utils/pcbManualEditEventHandler.ts +156 -0
  43. package/package.json +52 -0
  44. package/postcss.config.js +8 -0
  45. package/renovate.json +15 -0
  46. package/scripts/build-css.ts +26 -0
  47. package/src/main.tsx +13 -0
  48. package/tailwind.config.js +3 -0
  49. package/tsconfig.json +43 -0
  50. package/vite.config.ts +43 -0
@@ -0,0 +1,315 @@
1
+ import { useReducer, useState } from "react"
2
+ import { ClickableText } from "./ClickableText"
3
+ import { HeaderCell } from "./HeaderCell"
4
+ import Modal from "./Modal"
5
+
6
+ type Filters = {
7
+ component_type_filter?:
8
+ | "any"
9
+ | "source"
10
+ | "source/pcb"
11
+ | "source/schematic"
12
+ | string
13
+ id_search?: string
14
+ name_search?: string
15
+ selector_search?: string
16
+ focused_id?: string
17
+ }
18
+
19
+ type CommonProps = { name?: string; type: string }
20
+
21
+ type ModalState =
22
+ | { open: false; element?: never }
23
+ | { open: true; element: Element; title: string }
24
+
25
+ interface Element {
26
+ [key: string]: any
27
+ type: string
28
+ name?: string
29
+ }
30
+
31
+ interface ProcessedElement extends CommonProps {
32
+ primary_id: string
33
+ other_ids: { [key: string]: string }
34
+ selector_path?: string
35
+ _og_elm: Element
36
+ }
37
+
38
+ interface Column {
39
+ key: string
40
+ name: string
41
+ renderCell?: (row: ProcessedElement) => React.ReactNode
42
+ renderHeaderCell?: (col: Column) => React.ReactNode
43
+ }
44
+
45
+ export const CircuitJsonTableViewer: React.FC<{ elements: Element[] }> = ({
46
+ elements,
47
+ }) => {
48
+ const [modal, setModal] = useState<ModalState>({ open: false })
49
+ const [filters, setFilter] = useReducer(
50
+ (s: Filters, a: Filters) => ({
51
+ ...s,
52
+ ...a,
53
+ }),
54
+ {},
55
+ )
56
+
57
+ const element_types = [...new Set(elements.map((e) => e.type))]
58
+
59
+ // Process elements to separate primary and non-primary ids
60
+ const elements2: ProcessedElement[] = elements.map((e) => {
61
+ const primary_id = e[`${e.type}_id`]
62
+
63
+ const other_ids = Object.fromEntries(
64
+ Object.entries(e).filter(([k]) => {
65
+ if (k === `${e.type}_id`) return false
66
+ if (!k.endsWith("_id")) return false
67
+ return true
68
+ }),
69
+ ) as { [key: string]: string }
70
+
71
+ const other_props: CommonProps = Object.fromEntries(
72
+ Object.entries(e).filter(([k]) => !k.endsWith("_id")),
73
+ ) as CommonProps
74
+
75
+ return {
76
+ primary_id,
77
+ other_ids,
78
+ ...other_props,
79
+ _og_elm: e,
80
+ }
81
+ })
82
+
83
+ const elements3 = elements2.map((e) => {
84
+ let selector_path = ""
85
+
86
+ const getSelectorPath = (e2: ProcessedElement): string => {
87
+ const parent_key = Object.keys(e2.other_ids).find((k) =>
88
+ k.startsWith("source_"),
89
+ )
90
+ if (!parent_key) return `.${e2.name}`
91
+ const parent_type = parent_key.slice(0, -3) // trim "_id"
92
+
93
+ const parent = elements2.find(
94
+ (p) =>
95
+ p.type === parent_type && p.primary_id === e2.other_ids[parent_key],
96
+ )
97
+
98
+ if (!parent) return `??? > .${e2.name}`
99
+
100
+ if (!("name" in parent)) return `#${parent.primary_id} > .${e2.name}`
101
+
102
+ return `${getSelectorPath(parent)} > .${e2.name}`
103
+ }
104
+
105
+ if ("name" in e) {
106
+ selector_path = getSelectorPath(e)
107
+ }
108
+
109
+ return {
110
+ ...e,
111
+ selector_path,
112
+ }
113
+ })
114
+
115
+ const columns: Column[] = [
116
+ {
117
+ key: "primary_id",
118
+ name: "primary_id",
119
+ renderCell: (row: ProcessedElement) => (
120
+ <div className="flex items-center">
121
+ <ClickableText
122
+ text={row.primary_id}
123
+ onClick={() =>
124
+ setFilter({
125
+ focused_id: row.primary_id,
126
+ id_search: undefined,
127
+ selector_search: undefined,
128
+ })
129
+ }
130
+ />
131
+ <span className="flex-grow" />
132
+ <ClickableText
133
+ text="(JSON)"
134
+ onClick={() =>
135
+ setModal({
136
+ open: true,
137
+ element: row._og_elm,
138
+ title: row.primary_id,
139
+ })
140
+ }
141
+ />
142
+ </div>
143
+ ),
144
+ renderHeaderCell: (col: Column) => (
145
+ <HeaderCell
146
+ column={col}
147
+ onTextChange={(v) => setFilter({ id_search: v })}
148
+ field={
149
+ !filters.focused_id
150
+ ? undefined
151
+ : () => (
152
+ <div>
153
+ Focus:{" "}
154
+ <span className="underline">{filters.focused_id}</span>
155
+ <ClickableText
156
+ text="(unfocus)"
157
+ onClick={() => setFilter({ focused_id: undefined })}
158
+ />
159
+ </div>
160
+ )
161
+ }
162
+ />
163
+ ),
164
+ },
165
+ {
166
+ key: "type",
167
+ name: "type",
168
+ renderHeaderCell: (col: Column) => (
169
+ <HeaderCell
170
+ column={col}
171
+ field={() => (
172
+ <select
173
+ onChange={(e) =>
174
+ setFilter({ component_type_filter: e.target.value })
175
+ }
176
+ className="border rounded p-1 w-full"
177
+ >
178
+ <option key="any" value="any">
179
+ any
180
+ </option>
181
+ <option key="source" value="source">
182
+ source
183
+ </option>
184
+ <option key="source/pcb" value="source/pcb">
185
+ source/pcb
186
+ </option>
187
+ <option key="source/schematic" value="source/schematic">
188
+ source/schematic
189
+ </option>
190
+ {element_types.map((type) => (
191
+ <option key={type} value={type}>
192
+ {type}
193
+ </option>
194
+ ))}
195
+ </select>
196
+ )}
197
+ />
198
+ ),
199
+ },
200
+ {
201
+ key: "name",
202
+ name: "name",
203
+ renderHeaderCell: (col: Column) => (
204
+ <HeaderCell
205
+ column={col}
206
+ onTextChange={(t) => setFilter({ name_search: t })}
207
+ />
208
+ ),
209
+ },
210
+ {
211
+ key: "selector_path",
212
+ name: "selector_path",
213
+ renderHeaderCell: (col: Column) => (
214
+ <HeaderCell
215
+ column={col}
216
+ onTextChange={(t) => setFilter({ selector_search: t })}
217
+ />
218
+ ),
219
+ },
220
+ {
221
+ key: "other_ids",
222
+ name: "other_ids",
223
+ renderCell: (row: ProcessedElement) => (
224
+ <div className="space-x-2">
225
+ {Object.entries(row.other_ids).map(([other_id, v]) => (
226
+ <ClickableText
227
+ key={v}
228
+ text={v}
229
+ onClick={() => setFilter({ focused_id: v })}
230
+ />
231
+ ))}
232
+ </div>
233
+ ),
234
+ },
235
+ ]
236
+
237
+ const elements4 = elements3
238
+ .filter((e) => {
239
+ if (!filters.name_search) return true
240
+ return e.name?.toLowerCase()?.includes(filters.name_search.toLowerCase())
241
+ })
242
+ .filter((e) => {
243
+ if (!filters.component_type_filter) return true
244
+ if (filters.component_type_filter === "any") return true
245
+ if (filters.component_type_filter === "source") {
246
+ return e.type.startsWith("source_")
247
+ }
248
+ if (filters.component_type_filter === "source/pcb") {
249
+ return e.type.startsWith("source_") || e.type.startsWith("pcb_")
250
+ }
251
+ if (filters.component_type_filter === "source/schematic") {
252
+ return e.type.startsWith("source_") || e.type.startsWith("schematic_")
253
+ }
254
+ return e.type?.includes(filters.component_type_filter)
255
+ })
256
+ .filter((e) => {
257
+ if (!filters.selector_search) return true
258
+ const parts = filters.selector_search
259
+ .split(" ")
260
+ .filter((p) => p.length > 0)
261
+ return parts.every((part) => e.selector_path?.includes(part))
262
+ })
263
+ .filter((e) => {
264
+ if (!filters.id_search) return true
265
+ return e.primary_id?.includes(filters.id_search)
266
+ })
267
+ .filter((e) => {
268
+ if (!filters.focused_id) return true
269
+ if (e.primary_id === filters.focused_id) return true
270
+ if (Object.values(e.other_ids).includes(filters.focused_id)) return true
271
+ return false
272
+ })
273
+
274
+ return (
275
+ <div className="font-mono text-xs">
276
+ <div className="overflow-x-auto">
277
+ <table className="table-auto w-full text-left">
278
+ <thead>
279
+ <tr>
280
+ {columns.map((col) => (
281
+ <th key={col.key} className="px-4 py-2 border-b">
282
+ {col.renderHeaderCell ? col.renderHeaderCell(col) : col.name}
283
+ </th>
284
+ ))}
285
+ </tr>
286
+ </thead>
287
+ <tbody>
288
+ {elements4.map((row, rowIndex) => (
289
+ <tr key={rowIndex} className="hover:bg-gray-100">
290
+ {columns.map((col) => (
291
+ <td key={col.key} className="px-4 py-2 border-b">
292
+ {col.renderCell
293
+ ? col.renderCell(row)
294
+ : (row as any)[col.key]}
295
+ </td>
296
+ ))}
297
+ </tr>
298
+ ))}
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+ <Modal
303
+ open={modal.open}
304
+ onClose={() => setModal({ open: false })}
305
+ title={modal.open ? modal.title : ""}
306
+ >
307
+ <div className="bg-gray-800 p-3 text-white rounded">
308
+ <pre className="whitespace-pre-wrap">
309
+ {modal.open ? JSON.stringify(modal.element, null, 2) : ""}
310
+ </pre>
311
+ </div>
312
+ </Modal>
313
+ </div>
314
+ )
315
+ }
@@ -0,0 +1,20 @@
1
+ import type React from "react"
2
+
3
+ interface ClickableTextProps {
4
+ text: string
5
+ onClick: () => void
6
+ }
7
+
8
+ export const ClickableText: React.FC<ClickableTextProps> = ({
9
+ text,
10
+ onClick,
11
+ }) => {
12
+ return (
13
+ <span
14
+ className="cursor-pointer underline text-blue-300 mx-2"
15
+ onClick={onClick}
16
+ >
17
+ {text}
18
+ </span>
19
+ )
20
+ }
@@ -0,0 +1,26 @@
1
+ import type React from "react"
2
+
3
+ interface HeaderCellProps {
4
+ column: { name: string }
5
+ field?: () => React.ReactNode
6
+ onTextChange?: (value: string) => void
7
+ }
8
+
9
+ export const HeaderCell: React.FC<HeaderCellProps> = (p) => {
10
+ return (
11
+ <div className="leading-5">
12
+ <div className="py-2 font-bold">{p.column.name}</div>
13
+ <div>
14
+ {p.field?.() ?? (
15
+ <input
16
+ type="text"
17
+ className="border rounded p-1 w-full"
18
+ onChange={(e) => {
19
+ p.onTextChange?.(e.target.value)
20
+ }}
21
+ />
22
+ )}
23
+ </div>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,38 @@
1
+ import type React from "react"
2
+
3
+ interface ModalProps {
4
+ open: boolean
5
+ children: React.ReactNode
6
+ title: string
7
+ onClose: () => void
8
+ }
9
+
10
+ const Modal: React.FC<ModalProps> = ({ open, children, title, onClose }) => {
11
+ if (!open) return null
12
+
13
+ return (
14
+ <div
15
+ className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
16
+ onClick={onClose}
17
+ >
18
+ <div
19
+ className="bg-white p-5 rounded-lg relative w-11/12 max-w-2xl"
20
+ onClick={(e) => {
21
+ e.preventDefault()
22
+ e.stopPropagation()
23
+ }}
24
+ >
25
+ <h2 className="mt-0 text-xl">{title}</h2>
26
+ <button
27
+ className="absolute top-4 right-4 text-2xl font-bold"
28
+ onClick={onClose}
29
+ >
30
+ &times;
31
+ </button>
32
+ {children}
33
+ </div>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ export default Modal
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+
3
+ export const ErrorFallback = ({ error }: { error: Error }) => {
4
+ return (
5
+ <div
6
+ data-testid="error-container"
7
+ className="error-container mt-4 bg-red-50 rounded-md border border-red-200"
8
+ >
9
+ <div className="p-4">
10
+ <h2 className="text-lg font-semibold text-red-800 mb-3">
11
+ Error Loading 3D Viewer
12
+ </h2>
13
+ <p className="text-xs font-mono whitespace-pre-wrap text-red-700">
14
+ {error.message}
15
+ </p>
16
+ <details
17
+ style={{ whiteSpace: "pre-wrap" }}
18
+ className="text-xs font-mono text-red-600 mt-2"
19
+ >
20
+ {error.stack}
21
+ </details>
22
+ </div>
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,86 @@
1
+ import { GitHubLogoIcon } from "@radix-ui/react-icons"
2
+ import { ClipboardIcon } from "lucide-react"
3
+ import { Button } from "lib/components/ui/button"
4
+
5
+ export const ErrorTabContent = ({
6
+ code,
7
+ isStreaming,
8
+ errorMessage,
9
+ }: {
10
+ code?: string
11
+ isStreaming?: boolean
12
+ errorMessage?: string | null
13
+ }) => {
14
+ if (!errorMessage) {
15
+ return (
16
+ <div className="mt-4 bg-green-50 rounded-md border border-green-200">
17
+ <div className="p-4">
18
+ <h3 className="text-lg font-semibold text-green-800 mb-3">
19
+ No Errors 👌
20
+ </h3>
21
+ <p className="text-sm text-green-700">
22
+ Your code is running without any errors.
23
+ </p>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ return (
30
+ <>
31
+ <div className="mt-4 bg-red-50 rounded-md border border-red-200 max-h-[500px] overflow-y-auto">
32
+ <div className="p-4">
33
+ <h3 className="text-lg font-semibold text-red-800 mb-3">Error</h3>
34
+ <p className="text-xs font-mono whitespace-pre-wrap text-red-600 mt-2">
35
+ {errorMessage}
36
+ </p>
37
+ </div>
38
+ </div>
39
+ <div className="flex gap-2 mt-4 justify-end">
40
+ <Button
41
+ variant="outline"
42
+ onClick={() => {
43
+ if (!errorMessage) return
44
+ navigator.clipboard.writeText(errorMessage)
45
+ alert("Error copied to clipboard!")
46
+ }}
47
+ >
48
+ <ClipboardIcon className="w-4 h-4 mr-2" />
49
+ Copy Error
50
+ </Button>
51
+ <Button
52
+ variant="outline"
53
+ onClick={() => {
54
+ window.alert(
55
+ "Not supported yet! Please report/upvote an issue on github.com/tscircuit/tscircuit",
56
+ )
57
+ // const title = `Error: ${errorMessage
58
+ // .replace("Render Error:", "")
59
+ // .replace(/\"_errors\":\[\]/g, "")
60
+ // .replace(/\{,/g, "{")
61
+ // .replace(/"_errors":\[/g, "")
62
+ // .replace(/[^a-zA-Z0-9 ]/g, " ")
63
+ // .replace(/\s+/g, " ")
64
+ // .replace(/ \d+ /g, " ")
65
+ // .slice(0, 100)}`
66
+ // const url = encodeTextToUrlHash(code ?? "").replace(
67
+ // "http://localhost:5173",
68
+ // "https://snippets.tscircuit.com",
69
+ // )
70
+ // let body = `[Snippet code to reproduce](${url})\n\n### Error\n\`\`\`\n${errorMessage.slice(0, 600)}\n\`\`\``
71
+ // if (body.length > 4000) {
72
+ // body = `\`\`\`tsx\n// Please paste the code here\`\`\`\n\n### Error\n\`\`\`\n${errorMessage.slice(0, 2000)}\n\`\`\``
73
+ // }
74
+ // window.open(
75
+ // `https://github.com/tscircuit/snippets/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`,
76
+ // "_blank",
77
+ // )
78
+ }}
79
+ >
80
+ <GitHubLogoIcon className="w-4 h-4 mr-2" />
81
+ Report Issue
82
+ </Button>
83
+ </div>
84
+ </>
85
+ )
86
+ }
@@ -0,0 +1,47 @@
1
+ import { useRef, useState, useLayoutEffect, type ComponentProps } from "react"
2
+ import { PCBViewer } from "@tscircuit/pcb-viewer"
3
+
4
+ export const PcbViewerWithContainerHeight = ({
5
+ containerClassName,
6
+ ...props
7
+ }: {
8
+ containerClassName?: string
9
+ } & ComponentProps<typeof PCBViewer>) => {
10
+ const containerRef = useRef<HTMLDivElement>(null)
11
+ const [computedHeight, setComputedHeight] = useState(620)
12
+
13
+ useLayoutEffect(() => {
14
+ const updateHeight = () => {
15
+ if (containerRef.current) {
16
+ const containerHeight = containerRef.current.clientHeight
17
+ const screenHeight = window.innerHeight
18
+ setComputedHeight(
19
+ Math.min(Math.max(containerHeight, 620), screenHeight),
20
+ )
21
+ }
22
+ }
23
+
24
+ // Immediate synchronous calculation
25
+ updateHeight()
26
+
27
+ // Resize listener for dynamic changes
28
+ const resizeObserver = new ResizeObserver(updateHeight)
29
+ if (containerRef.current) {
30
+ resizeObserver.observe(containerRef.current)
31
+ }
32
+
33
+ // Fallback for window resize
34
+ window.addEventListener("resize", updateHeight)
35
+
36
+ return () => {
37
+ resizeObserver.disconnect()
38
+ window.removeEventListener("resize", updateHeight)
39
+ }
40
+ }, [])
41
+
42
+ return (
43
+ <div ref={containerRef} className={containerClassName || "w-full h-full"}>
44
+ <PCBViewer {...props} height={computedHeight} />
45
+ </div>
46
+ )
47
+ }
@@ -0,0 +1,14 @@
1
+ import { Button } from "lib/components/ui/button"
2
+ import { PlayIcon } from "lucide-react"
3
+
4
+ const PreviewEmptyState = ({ onRunClicked }: { onRunClicked: () => void }) => (
5
+ <div className="flex items-center gap-3 bg-gray-100 text-center justify-center py-10">
6
+ No circuit json loaded
7
+ <Button className="bg-blue-600 hover:bg-blue-500" onClick={onRunClicked}>
8
+ Run Code
9
+ <PlayIcon className="w-3 h-3 ml-2" />
10
+ </Button>
11
+ </div>
12
+ )
13
+
14
+ export default PreviewEmptyState
@@ -0,0 +1,68 @@
1
+ import { createCircuitWebWorker } from "@tscircuit/eval-webworker"
2
+ import { CircuitJsonPreview } from "./CircuitJsonPreview"
3
+ import { useEffect, useState } from "react"
4
+
5
+ // @ts-ignore
6
+ import evalWebWorkerBlobUrl from "@tscircuit/eval-webworker/blob-url"
7
+
8
+ interface Props {
9
+ /**
10
+ * Map of filenames to file contents that will be available in the worker
11
+ */
12
+ fsMap: { [filename: string]: string }
13
+
14
+ /**
15
+ * The entry point file that will be executed first
16
+ */
17
+ entrypoint: string
18
+
19
+ /**
20
+ * Called when the circuit JSON changes
21
+ */
22
+ onCircuitJsonChange?: (circuitJson: any) => void
23
+
24
+ /**
25
+ * Called when rendering is finished
26
+ */
27
+ onRenderingFinished?: (params: { circuitJson: any }) => void
28
+
29
+ /**
30
+ * Called for each render event
31
+ */
32
+ onRenderEvent?: (event: any) => void
33
+
34
+ /**
35
+ * Called when an error occurs
36
+ */
37
+ onError?: (error: Error) => void
38
+ }
39
+
40
+ export const RunFrame = (props: Props) => {
41
+ const [circuitJson, setCircuitJson] = useState<any>(null)
42
+
43
+ useEffect(() => {
44
+ async function runWorker() {
45
+ const worker = await createCircuitWebWorker({
46
+ webWorkerUrl: evalWebWorkerBlobUrl,
47
+ verbose: true,
48
+ })
49
+ const $finished = worker.executeWithFsMap({
50
+ entrypoint: props.entrypoint,
51
+ fsMap: props.fsMap,
52
+ })
53
+ console.log("waiting for execution to finish...")
54
+ await $finished
55
+ console.log("waiting for initial circuit json...")
56
+ setCircuitJson(await worker.getCircuitJson())
57
+ console.log("got initial circuit json")
58
+ await $finished.catch((e) => {
59
+ console.error(e)
60
+ })
61
+ setCircuitJson(await worker.getCircuitJson())
62
+ }
63
+ runWorker()
64
+ }, [props.fsMap])
65
+
66
+ console.log({ circuitJson })
67
+ return <CircuitJsonPreview circuitJson={circuitJson} />
68
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect } from "react"
2
+ import { RunFrame } from "../RunFrame"
3
+ import { useRunFrameStore, selectCurrentFileMap } from "./store"
4
+ import type { RunFrameWithApiProps } from "./types"
5
+
6
+ const guessEntrypoint = (files: string[]) =>
7
+ files.find((file) => file.endsWith(".tsx"))
8
+
9
+ export const RunFrameWithApi = ({ apiBaseUrl }: RunFrameWithApiProps) => {
10
+ const { startPolling, stopPolling } = useRunFrameStore()
11
+ const fsMap = useRunFrameStore(selectCurrentFileMap)
12
+
13
+ // Initialize API base URL
14
+ useEffect(() => {
15
+ if (apiBaseUrl) {
16
+ window.API_BASE_URL = apiBaseUrl
17
+ }
18
+ }, [apiBaseUrl])
19
+
20
+ // Start/stop polling
21
+ useEffect(() => {
22
+ startPolling()
23
+ return () => stopPolling()
24
+ }, [startPolling, stopPolling])
25
+
26
+ const entrypoint = guessEntrypoint(Object.keys(fsMap))
27
+
28
+ if (!entrypoint) {
29
+ return <div>No entrypoint found for Run Frame!</div>
30
+ }
31
+
32
+ return (
33
+ <RunFrame
34
+ fsMap={fsMap}
35
+ entrypoint={entrypoint}
36
+ onError={(error) => {
37
+ console.error("RunFrame error:", error)
38
+ }}
39
+ />
40
+ )
41
+ }
@@ -0,0 +1,6 @@
1
+ import { createRoot } from "react-dom/client"
2
+ import { RunFrameWithApi } from "./index"
3
+
4
+ const root = createRoot(document.getElementById("root")!)
5
+
6
+ root.render(<RunFrameWithApi />)