1
- import { Input } from "../ui/input " ;
2
- import { ProgramUploadFileOutput } from "./types" ;
1
+ import { useDropzone } from "react-dropzone " ;
2
+ import { ProgramUploadFileInput , ProgramUploadFileOutput } from "./types" ;
3
3
import { mapUploadFileInputToOutput } from "./utils" ;
4
4
import { decodeStandardProgram } from "@typeberry/pvm-debugger-adapter" ;
5
5
import { MemoryChunkItem , PageMapItem , RegistersArray } from "@/types/pvm.ts" ;
6
6
import { SafeParseReturnType , z } from "zod" ;
7
7
import { useAppSelector } from "@/store/hooks" ;
8
8
import { selectInitialState } from "@/store/debugger/debuggerSlice" ;
9
9
import { getAsChunks , getAsPageMap } from "@/lib/utils.ts" ;
10
+ import { TriangleAlert , UploadCloud } from "lucide-react" ;
11
+ import { Button } from "../ui/button" ;
12
+ import { useState } from "react" ;
10
13
11
- const validateJsonTestCaseSchema = ( json : unknown ) => {
14
+ type ProgramFileUploadProps = {
15
+ onFileUpload : ( val : ProgramUploadFileOutput ) => void ;
16
+ close ?: ( ) => void ;
17
+ } ;
18
+
19
+ type RawProgramUploadFileInput = unknown ;
20
+ type ValidationResult = SafeParseReturnType < RawProgramUploadFileInput , ProgramUploadFileInput > ;
21
+
22
+ const validateJsonTestCaseSchema = ( json : RawProgramUploadFileInput ) : ValidationResult => {
12
23
const pageMapSchema = z . object ( {
13
24
address : z . number ( ) ,
14
25
length : z . number ( ) ,
@@ -38,10 +49,8 @@ const validateJsonTestCaseSchema = (json: unknown) => {
38
49
return schema . safeParse ( json ) ;
39
50
} ;
40
51
41
- const generateErrorMessageFromZodValidation = ( result : SafeParseReturnType < unknown , unknown > ) => {
42
- if ( ! result . error ) {
43
- return false ;
44
- }
52
+ const generateErrorMessageFromZodValidation = ( result : ValidationResult ) : string => {
53
+ if ( ! result . error ) return "Unknown error occurred" ;
45
54
46
55
const formattedErrors = result . error . errors . map ( ( err ) => {
47
56
const path = err . path . join ( " > " ) || "root" ;
@@ -51,18 +60,9 @@ const generateErrorMessageFromZodValidation = (result: SafeParseReturnType<unkno
51
60
return `File validation failed with the following errors:\n\n${ formattedErrors . join ( "\n" ) } ` ;
52
61
} ;
53
62
54
- export const ProgramFileUpload = ( {
55
- onFileUpload,
56
- onParseError,
57
- close,
58
- } : {
59
- onFileUpload : ( val : ProgramUploadFileOutput ) => void ;
60
- onParseError : ( error : string ) => void ;
61
- close ?: ( ) => void ;
62
- } ) => {
63
+ export const ProgramFileUpload : React . FC < ProgramFileUploadProps > = ( { onFileUpload, close } ) => {
63
64
const initialState = useAppSelector ( selectInitialState ) ;
64
-
65
- let fileReader : FileReader ;
65
+ const [ error , setError ] = useState < string > ( ) ;
66
66
67
67
const handleFileRead = ( e : ProgressEvent < FileReader > ) => {
68
68
const arrayBuffer = e . target ?. result ;
@@ -78,7 +78,7 @@ export const ProgramFileUpload = ({
78
78
79
79
if ( ! result . success ) {
80
80
const errorMessage = generateErrorMessageFromZodValidation ( result ) ;
81
- onParseError ( errorMessage || "" ) ;
81
+ setError ( errorMessage || "" ) ;
82
82
} else {
83
83
onFileUpload ( mapUploadFileInputToOutput ( jsonFile ) ) ;
84
84
}
@@ -112,30 +112,48 @@ export const ProgramFileUpload = ({
112
112
} ) ;
113
113
}
114
114
}
115
+ } else {
116
+ setError ( "Failed to read the file" ) ;
115
117
}
116
118
} ;
117
119
118
- const handleProgramUpload = ( file : Blob ) => {
119
- fileReader = new FileReader ( ) ;
120
- fileReader . onloadend = handleFileRead ;
121
- fileReader . readAsArrayBuffer ( file ) ;
120
+ const onDrop = ( acceptedFiles : File [ ] ) => {
121
+ if ( acceptedFiles . length ) {
122
+ const file = acceptedFiles [ 0 ] ;
123
+ const fileReader = new FileReader ( ) ;
124
+ fileReader . onloadend = handleFileRead ;
125
+ fileReader . readAsArrayBuffer ( file ) ;
126
+ close ?.( ) ;
127
+ }
122
128
} ;
123
129
130
+ const { getRootProps, getInputProps, open } = useDropzone ( {
131
+ onDrop,
132
+ accept : { "application/octet-stream" : [ ".bin" , ".pvm" ] , "application/json" : [ ".json" ] } ,
133
+ noClick : true ,
134
+ } ) ;
135
+
124
136
return (
125
- < div className = "pb-5" >
126
- < Input
127
- className = "mt-5 mr-3"
128
- id = "test-file"
129
- type = "file"
130
- accept = ".bin,.pvm,.json"
131
- onClick = { ( e ) => e . stopPropagation ( ) }
132
- onChange = { ( e ) => {
133
- if ( e . target . files ?. length ) {
134
- handleProgramUpload ( e . target . files [ 0 ] ) ;
135
- close ?.( ) ;
136
- }
137
- } }
138
- />
137
+ < div >
138
+ < div
139
+ { ...getRootProps ( ) }
140
+ className = "flex items-center justify-between border-dashed border-2 py-3 px-4 rounded-lg w-full mx-auto"
141
+ >
142
+ < div className = "flex items-center space-x-6" >
143
+ < UploadCloud className = "text-title-secondary-foreground" width = "30px" height = "30px" />
144
+ < p className = "text-[10px] text-title-secondary-foreground" > Select a file or drag and drop here</ p >
145
+ </ div >
146
+ < Button className = "text-[10px] py-1 h-9" variant = "outlineBrand" onClick = { open } >
147
+ Select file
148
+ </ Button >
149
+ < input { ...getInputProps ( ) } className = "hidden" />
150
+ </ div >
151
+
152
+ { error && (
153
+ < p className = "flex items-center text-destructive-foreground mt-3 text-[11px] whitespace-pre-line" >
154
+ < TriangleAlert className = "mr-2" height = "18px" /> { error }
155
+ </ p >
156
+ ) }
139
157
</ div >
140
158
) ;
141
159
} ;
0 commit comments