11import { Editor } from "@/client/Editor" ;
22import { Preview } from "@/client/Preview" ;
3- import { Logo } from "@/client/components/Logo" ;
4- import {
5- ResizableHandle ,
6- ResizablePanelGroup ,
7- } from "@/client/components/Resizable" ;
8- import { useStore } from "@/client/store" ;
3+ import { Button } from "@/client/components/Button" ;
94import {
105 DropdownMenu ,
116 DropdownMenuContent ,
127 DropdownMenuItem ,
138 DropdownMenuPortal ,
149 DropdownMenuTrigger ,
1510} from "@/client/components/DropdownMenu" ;
11+ import { Logo } from "@/client/components/Logo" ;
1612import {
17- type FC ,
18- useCallback ,
19- useEffect ,
20- useMemo ,
21- useRef ,
22- useState ,
23- } from "react" ;
24- import { useTheme } from "@/client/contexts/theme" ;
25- import { MoonIcon , ShareIcon , SunIcon , SunMoonIcon } from "lucide-react" ;
26- import { Button } from "@/client/components/Button" ;
13+ ResizableHandle ,
14+ ResizablePanelGroup ,
15+ } from "@/client/components/Resizable" ;
2716import {
2817 Tooltip ,
2918 TooltipContent ,
3019 TooltipTrigger ,
3120} from "@/client/components/Tooltip" ;
21+ import { useTheme } from "@/client/contexts/theme" ;
22+ import { defaultCode } from "@/client/snippets" ;
23+ import type {
24+ ParameterWithSource ,
25+ PreviewOutput ,
26+ WorkspaceOwner ,
27+ } from "@/gen/types" ;
3228import { rpc } from "@/utils/rpc" ;
33- import { useLoaderData , type LoaderFunctionArgs } from "react-router" ;
34- import type { WorkspaceOwner } from "@/gen/types.ts" ;
35-
36- type GoPreviewDef = (
37- v : Record < string , string > ,
38- owner : WorkspaceOwner ,
39- params : Record < string , string > ,
40- ) => Promise < string > ;
41-
42- // Extend the Window object to include the Go related code that is added from
43- // wasm_exec.js and our loaded Go code.
44- declare global {
45- interface Window {
46- // Loaded from wasm
47- go_preview ?: GoPreviewDef ;
48- Go : { new ( ) : Go } ;
49- CODE ?: string ;
50- }
51- }
52-
53- declare class Go {
54- argv : string [ ] ;
55- env : { [ envKey : string ] : string } ;
56- exit : ( code : number ) => void ;
57- importObject : WebAssembly . Imports ;
58- exited : boolean ;
59- mem : DataView ;
60- run ( instance : WebAssembly . Instance ) : Promise < void > ;
61- }
29+ import {
30+ type WasmLoadState ,
31+ getDynamicParametersOutput ,
32+ initWasm ,
33+ } from "@/utils/wasm" ;
34+ import isEqual from "lodash/isEqual" ;
35+ import { MoonIcon , ShareIcon , SunIcon , SunMoonIcon } from "lucide-react" ;
36+ import { type FC , useEffect , useMemo , useRef , useState } from "react" ;
37+ import { type LoaderFunctionArgs , useLoaderData } from "react-router" ;
38+ import { useDebouncedValue } from "./hooks/debounce" ;
39+ import { mockUsers } from "@/owner" ;
6240
41+ /**
42+ * Load the shared code if present.
43+ */
6344export const loader = async ( { params } : LoaderFunctionArgs ) => {
6445 const { id } = params ;
6546 if ( ! id ) {
@@ -79,45 +60,116 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
7960} ;
8061
8162export const App = ( ) => {
82- const $wasmState = useStore ( ( state ) => state . wasmState ) ;
83- const $setWasmState = useStore ( ( state ) => state . setWasmState ) ;
84- const $setCode = useStore ( ( store ) => store . setCode ) ;
85- const code = useLoaderData < typeof loader > ( ) ;
63+ const [ wasmLoadState , setWasmLoadingState ] = useState < WasmLoadState > ( ( ) => {
64+ if ( window . go_preview ) {
65+ return "loaded" ;
66+ }
67+ return "loading" ;
68+ } ) ;
69+ const loadedCode = useLoaderData < typeof loader > ( ) ;
70+ const [ code , setCode ] = useState ( loadedCode ?? defaultCode ) ;
71+ const [ debouncedCode , isDebouncing ] = useDebouncedValue ( code , 1000 ) ;
72+ const [ parameterValues , setParameterValues ] = useState <
73+ Record < string , string >
74+ > ( { } ) ;
75+ const [ output , setOutput ] = useState < PreviewOutput | null > ( null ) ;
76+ const [ parameters , setParameters ] = useState < ParameterWithSource [ ] > ( [ ] ) ;
77+ const [ owner , setOwner ] = useState < WorkspaceOwner > ( mockUsers . admin ) ;
78+
79+ const onDownloadOutput = ( ) => {
80+ const blob = new Blob ( [ JSON . stringify ( output , null , 2 ) ] , {
81+ type : "application/json" ,
82+ } ) ;
83+
84+ const url = URL . createObjectURL ( blob ) ;
85+
86+ const link = document . createElement ( "a" ) ;
87+ link . href = url ;
88+ link . download = "output.json" ;
89+ document . body . appendChild ( link ) ;
90+ link . click ( ) ;
91+ document . body . removeChild ( link ) ;
92+
93+ // Give the click event enough time to fire and then revoke the URL.
94+ // This method of doing it doesn't seem great but I'm not sure if there is a
95+ // better way.
96+ setTimeout ( ( ) => {
97+ URL . revokeObjectURL ( url ) ;
98+ } , 100 ) ;
99+ } ;
100+
101+ const onReset = ( ) => {
102+ setParameterValues ( { } ) ;
103+ setParameters ( ( curr ) =>
104+ curr . map ( ( p ) => {
105+ p . uuid = window . crypto . randomUUID ( ) ;
106+ return p ;
107+ } ) ,
108+ ) ;
109+ } ;
86110
87111 useEffect ( ( ) => {
88- if ( ! code ) {
89- return ;
112+ if ( ! window . go_preview ) {
113+ initWasm ( ) . then ( ( loadState ) => {
114+ setWasmLoadingState ( loadState ) ;
115+ } ) ;
116+ } else {
117+ // We assume that if `window.go_preview` has already created then the wasm
118+ // has already been initiated.
119+ setWasmLoadingState ( "loaded" ) ;
90120 }
121+ } , [ ] ) ;
122+
123+ useEffect ( ( ) => {
124+ setParameters ( ( curr ) => {
125+ const newParameters = output ?. output ?. parameters ?? [ ] ;
91126
92- $setCode ( code ) ;
93- } , [ code , $setCode ] ) ;
127+ return newParameters . map ( ( p ) => {
128+ // Check if the parameter is already in the array and if it is then keep it.
129+ // This allows us to optimize React by not re-rendering parameters that haven't changed.
130+ //
131+ // We unset value because the value may not be in sync with what we have locally,
132+ // and we unset uuid because it's given a new random UUID every time.
133+ const existing = curr . find ( ( currP ) => {
134+ const currentParameterOmitValue = {
135+ ...currP ,
136+ value : undefined ,
137+ uuid : undefined ,
138+ } ;
139+ const existingParameterOmitValue = {
140+ ...p ,
141+ value : undefined ,
142+ uuid : undefined ,
143+ } ;
144+
145+ return isEqual ( currentParameterOmitValue , existingParameterOmitValue ) ;
146+ } ) ;
147+
148+ if ( existing ) {
149+ existing . value = p . value ;
150+ return existing ;
151+ }
152+ return p ;
153+ } ) ;
154+ } ) ;
155+ } , [ output ] ) ;
94156
95- // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
96157 useEffect ( ( ) => {
97- const initWasm = async ( ) => {
98- try {
99- const goWasm = new window . Go ( ) ;
100- const result = await WebAssembly . instantiateStreaming (
101- fetch (
102- import . meta. env . PROD
103- ? "/assets/build/preview.wasm"
104- : "/build/preview.wasm" ,
105- ) ,
106- goWasm . importObject ,
107- ) ;
108-
109- goWasm . run ( result . instance ) ;
110- $setWasmState ( "loaded" ) ;
111- } catch ( e ) {
112- $setWasmState ( "error" ) ;
158+ if ( wasmLoadState !== "loaded" ) {
159+ return ;
160+ }
161+
162+ getDynamicParametersOutput ( debouncedCode , parameterValues , owner )
163+ . catch ( ( e ) => {
113164 console . error ( e ) ;
114- }
115- } ;
165+ setWasmLoadingState ( "error" ) ;
116166
117- if ( $wasmState !== "loaded" ) {
118- initWasm ( ) ;
119- }
120- } , [ ] ) ;
167+ return null ;
168+ } )
169+ . then ( ( output ) => {
170+ setOutput ( output ) ;
171+ } ) ;
172+ } , [ debouncedCode , parameterValues , wasmLoadState , owner ] ) ;
121173
122174 return (
123175 < main className = "flex h-dvh w-screen flex-col items-center bg-surface-primary" >
@@ -131,7 +183,7 @@ export const App = () => {
131183 </ p >
132184 </ div >
133185
134- < ShareButton />
186+ < ShareButton code = { code } />
135187 </ div >
136188
137189 < div className = "flex items-center gap-3" >
@@ -163,14 +215,27 @@ export const App = () => {
163215 </ div >
164216 </ nav >
165217
166- < ResizablePanelGroup aria-hidden = { ! $wasmState } direction = { "horizontal" } >
218+ < ResizablePanelGroup direction = { "horizontal" } >
167219 { /* EDITOR */ }
168- < Editor />
220+ < Editor code = { code } setCode = { setCode } />
169221
170222 < ResizableHandle className = "bg-surface-quaternary" />
171223
172224 { /* PREVIEW */ }
173- < Preview />
225+ < Preview
226+ wasmLoadState = { wasmLoadState }
227+ isDebouncing = { isDebouncing }
228+ onDownloadOutput = { onDownloadOutput }
229+ output = { output }
230+ parameterValues = { parameterValues }
231+ setParameterValues = { setParameterValues }
232+ parameters = { parameters }
233+ onReset = { onReset }
234+ setOwner = { ( owner ) => {
235+ onReset ( ) ;
236+ setOwner ( owner ) ;
237+ } }
238+ />
174239 </ ResizablePanelGroup >
175240 </ main >
176241 ) ;
@@ -215,15 +280,17 @@ const ThemeSelector: FC = () => {
215280 ) ;
216281} ;
217282
218- const ShareButton : FC = ( ) => {
219- const $code = useStore ( ( state ) => state . code ) ;
283+ type ShareButtonProps = {
284+ code : string ;
285+ } ;
286+ const ShareButton : FC < ShareButtonProps > = ( { code } ) => {
220287 const [ isCopied , setIsCopied ] = useState ( ( ) => false ) ;
221288 const timeoutId = useRef < ReturnType < typeof setTimeout > > ( undefined ) ;
222289
223- const onShare = useCallback ( async ( ) => {
290+ const onShare = async ( ) => {
224291 try {
225292 const { id } = await rpc . parameters
226- . $post ( { json : { code : $code } } )
293+ . $post ( { json : { code } } )
227294 . then ( ( res ) => res . json ( ) ) ;
228295
229296 const { protocol, host } = window . location ;
@@ -235,7 +302,7 @@ const ShareButton: FC = () => {
235302 } catch ( e ) {
236303 console . error ( e ) ;
237304 }
238- } , [ $code ] ) ;
305+ } ;
239306
240307 useEffect ( ( ) => {
241308 if ( ! isCopied ) {
0 commit comments