1
1
import { Editor } from "@/client/Editor" ;
2
2
import { 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" ;
9
4
import {
10
5
DropdownMenu ,
11
6
DropdownMenuContent ,
12
7
DropdownMenuItem ,
13
8
DropdownMenuPortal ,
14
9
DropdownMenuTrigger ,
15
10
} from "@/client/components/DropdownMenu" ;
11
+ import { Logo } from "@/client/components/Logo" ;
16
12
import {
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" ;
27
16
import {
28
17
Tooltip ,
29
18
TooltipContent ,
30
19
TooltipTrigger ,
31
20
} 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" ;
32
28
import { 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" ;
62
40
41
+ /**
42
+ * Load the shared code if present.
43
+ */
63
44
export const loader = async ( { params } : LoaderFunctionArgs ) => {
64
45
const { id } = params ;
65
46
if ( ! id ) {
@@ -79,45 +60,116 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
79
60
} ;
80
61
81
62
export 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
+ } ;
86
110
87
111
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" ) ;
90
120
}
121
+ } , [ ] ) ;
122
+
123
+ useEffect ( ( ) => {
124
+ setParameters ( ( curr ) => {
125
+ const newParameters = output ?. output ?. parameters ?? [ ] ;
91
126
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 ] ) ;
94
156
95
- // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
96
157
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 ) => {
113
164
console . error ( e ) ;
114
- }
115
- } ;
165
+ setWasmLoadingState ( "error" ) ;
116
166
117
- if ( $wasmState !== "loaded" ) {
118
- initWasm ( ) ;
119
- }
120
- } , [ ] ) ;
167
+ return null ;
168
+ } )
169
+ . then ( ( output ) => {
170
+ setOutput ( output ) ;
171
+ } ) ;
172
+ } , [ debouncedCode , parameterValues , wasmLoadState , owner ] ) ;
121
173
122
174
return (
123
175
< main className = "flex h-dvh w-screen flex-col items-center bg-surface-primary" >
@@ -131,7 +183,7 @@ export const App = () => {
131
183
</ p >
132
184
</ div >
133
185
134
- < ShareButton />
186
+ < ShareButton code = { code } />
135
187
</ div >
136
188
137
189
< div className = "flex items-center gap-3" >
@@ -163,14 +215,27 @@ export const App = () => {
163
215
</ div >
164
216
</ nav >
165
217
166
- < ResizablePanelGroup aria-hidden = { ! $wasmState } direction = { "horizontal" } >
218
+ < ResizablePanelGroup direction = { "horizontal" } >
167
219
{ /* EDITOR */ }
168
- < Editor />
220
+ < Editor code = { code } setCode = { setCode } />
169
221
170
222
< ResizableHandle className = "bg-surface-quaternary" />
171
223
172
224
{ /* 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
+ />
174
239
</ ResizablePanelGroup >
175
240
</ main >
176
241
) ;
@@ -215,15 +280,17 @@ const ThemeSelector: FC = () => {
215
280
) ;
216
281
} ;
217
282
218
- const ShareButton : FC = ( ) => {
219
- const $code = useStore ( ( state ) => state . code ) ;
283
+ type ShareButtonProps = {
284
+ code : string ;
285
+ } ;
286
+ const ShareButton : FC < ShareButtonProps > = ( { code } ) => {
220
287
const [ isCopied , setIsCopied ] = useState ( ( ) => false ) ;
221
288
const timeoutId = useRef < ReturnType < typeof setTimeout > > ( undefined ) ;
222
289
223
- const onShare = useCallback ( async ( ) => {
290
+ const onShare = async ( ) => {
224
291
try {
225
292
const { id } = await rpc . parameters
226
- . $post ( { json : { code : $code } } )
293
+ . $post ( { json : { code } } )
227
294
. then ( ( res ) => res . json ( ) ) ;
228
295
229
296
const { protocol, host } = window . location ;
@@ -235,7 +302,7 @@ const ShareButton: FC = () => {
235
302
} catch ( e ) {
236
303
console . error ( e ) ;
237
304
}
238
- } , [ $code ] ) ;
305
+ } ;
239
306
240
307
useEffect ( ( ) => {
241
308
if ( ! isCopied ) {
0 commit comments