diff --git a/package-lock.json b/package-lock.json index 0fbc062..dc852d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,8 @@ "react-toastify": "^10.0.6", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.1" + "vaul": "^0.9.1", + "zod": "^3.24.1" }, "devDependencies": { "@playwright/test": "^1.47.2", @@ -7460,6 +7461,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index f124762..918029c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "react-toastify": "^10.0.6", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.1" + "vaul": "^0.9.1", + "zod": "^3.24.1" }, "devDependencies": { "@playwright/test": "^1.47.2", diff --git a/src/components/Instructions/InstructionItem.tsx b/src/components/Instructions/InstructionItem.tsx index 525ee2c..2b26b57 100644 --- a/src/components/Instructions/InstructionItem.tsx +++ b/src/components/Instructions/InstructionItem.tsx @@ -9,8 +9,9 @@ import { TableCell, TableRow } from "../ui/table"; import { ProgramRow } from "."; import { useAppSelector } from "@/store/hooks.ts"; import { selectWorkers, WorkerState } from "@/store/workers/workersSlice.ts"; -import { hexToRgb } from "@/lib/utils.ts"; +import { hexToRgb, invertHexColor } from "@/lib/utils.ts"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip.tsx"; +import useDarkMode from "@/hooks/useDarkMode.ts"; const getWorkerValueFromState = ( worker: WorkerState, @@ -73,6 +74,7 @@ export const InstructionItem = forwardRef( ref: ForwardedRef, ) => { const { numeralSystem } = useContext(NumeralSystemContext); + const isDarkMode = useDarkMode(); const workers = useAppSelector(selectWorkers); const workersWithCurrentPc = workers.filter((worker) => worker.currentState.pc === programRow.address); @@ -81,7 +83,7 @@ export const InstructionItem = forwardRef( onClick(programRow); }, [programRow, onClick]); - const { backgroundColor, hasTooltip } = getHighlightStatus(workers, programRow, status); + const { backgroundColor, hasTooltip } = getHighlightStatus(workers, programRow, status, isDarkMode); const renderContent = () => { return ( @@ -161,7 +163,7 @@ export const InstructionItem = forwardRef( }, ); -function getHighlightStatus(workers: WorkerState[], programRow: ProgramRow, status?: Status) { +function getHighlightStatus(workers: WorkerState[], programRow: ProgramRow, status?: Status, isDarkMode?: boolean) { const pcInAllWorkers = (state: "currentState" | "previousState") => workers.map((worker) => getWorkerValueFromState(worker, state, "pc")); @@ -177,7 +179,11 @@ function getHighlightStatus(workers: WorkerState[], programRow: ProgramRow, stat pcInAllWorkers("currentState").length; const blockBackground = programRow.block.number % 2 === 0 ? "#fff" : "#efefef"; - const backgroundColor = isHighlighted ? `rgba(${hexToRgb(bgColor)}, ${bgOpacity})` : blockBackground; + const backgroundColor = isHighlighted + ? `rgba(${hexToRgb(bgColor)}, ${bgOpacity})` + : isDarkMode + ? invertHexColor(blockBackground) + : blockBackground; return { backgroundColor, diff --git a/src/components/MemoryPreview/index.tsx b/src/components/MemoryPreview/index.tsx index 06036ed..034429a 100644 --- a/src/components/MemoryPreview/index.tsx +++ b/src/components/MemoryPreview/index.tsx @@ -51,8 +51,8 @@ const MemoryCell = ({ (); const [assembly, setAssembly] = useState(defaultAssembly); const [isFirstCompilation, setFirstCompilation] = useState(true); + const isDarkMode = useDarkMode(); // compile the assembly for the first time useEffect(() => { @@ -194,6 +196,7 @@ export const Assembly = ({ height="100%" placeholder="Try writing some PolkaVM assembly code." value={assembly} + theme={isDarkMode ? "dark" : "light"} onChange={(value) => compile(value)} /> diff --git a/src/components/ProgramLoader/BinaryFileUpload.tsx b/src/components/ProgramLoader/BinaryFileUpload.tsx deleted file mode 100644 index be9cf39..0000000 --- a/src/components/ProgramLoader/BinaryFileUpload.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Input } from "../ui/input"; -import { ProgramUploadFileOutput } from "./types"; -import { decodeStandardProgram } from "@typeberry/pvm-debugger-adapter"; -import { RegistersArray } from "@/types/pvm.ts"; - -export const BinaryFileUpload = ({ - onFileUpload, - close, -}: { - onFileUpload: (val: ProgramUploadFileOutput) => void; - close?: () => void; -}) => { - let fileReader: FileReader; - - const handleFileRead = (e: ProgressEvent) => { - const arrayBuffer = e.target?.result; - - if (arrayBuffer instanceof ArrayBuffer) { - const uint8Array = new Uint8Array(arrayBuffer); - const { code, /*memory,*/ registers } = decodeStandardProgram(uint8Array, new Uint8Array()); - - onFileUpload({ - program: Array.from(code), - name: "custom", - initial: { - regs: Array.from(registers) as RegistersArray, - pc: 0, - pageMap: [], - // TODO: map memory properly - // memory: [...memory], - gas: 10000n, - }, - }); - } else { - console.error("Unexpected result type:", arrayBuffer); - } - }; - - const handleProgramUpload = (file: Blob) => { - fileReader = new FileReader(); - fileReader.onload = handleFileRead; - fileReader.readAsArrayBuffer(file); - }; - - return ( -
-

Upload program as a binary file

- e.stopPropagation()} - onChange={(e) => { - if (e.target.files?.length) { - handleProgramUpload(e.target.files[0]); - close?.(); - } - }} - /> -
- ); -}; diff --git a/src/components/ProgramLoader/Bytecode.tsx b/src/components/ProgramLoader/Bytecode.tsx deleted file mode 100644 index c087dd9..0000000 --- a/src/components/ProgramLoader/Bytecode.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ProgramUploadFileOutput } from "./types"; -import { BinaryFileUpload } from "@/components/ProgramLoader/BinaryFileUpload.tsx"; - -export const Bytecode = ({ - onProgramLoad, -}: { - onProgramLoad: (val?: ProgramUploadFileOutput, error?: string) => void; -}) => { - const handleFileUpload = (val: ProgramUploadFileOutput) => { - onProgramLoad(val); - }; - - return ( -
- -
- ); -}; diff --git a/src/components/ProgramLoader/Examples.tsx b/src/components/ProgramLoader/Examples.tsx index 94f416e..ad7eb11 100644 --- a/src/components/ProgramLoader/Examples.tsx +++ b/src/components/ProgramLoader/Examples.tsx @@ -106,7 +106,7 @@ const programs: { export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUploadFileOutput) => void }) => { return (
-

Load example test file

+

Load an example test file

diff --git a/src/components/ProgramLoader/Loader.tsx b/src/components/ProgramLoader/Loader.tsx index 097335c..c49e4d4 100644 --- a/src/components/ProgramLoader/Loader.tsx +++ b/src/components/ProgramLoader/Loader.tsx @@ -1,8 +1,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Button } from "../ui/button"; -import { Bytecode } from "./Bytecode"; import { Examples } from "./Examples"; -import { TextFileUpload } from "./TextFileUpload"; import { useState, useCallback, useEffect } from "react"; import { ProgramUploadFileOutput } from "./types"; import { useDebuggerActions } from "@/hooks/useDebuggerActions"; @@ -10,9 +8,15 @@ import { useAppDispatch, useAppSelector } from "@/store/hooks.ts"; import { setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts"; import { selectIsAnyWorkerLoading } from "@/store/workers/workersSlice"; import { isSerializedError } from "@/store/utils"; +import { ProgramFileUpload } from "@/components/ProgramLoader/ProgramFileUpload.tsx"; +import { bytes } from "@typeberry/block"; +import { selectInitialState } from "@/store/debugger/debuggerSlice.ts"; +import { decodeStandardProgram } from "@typeberry/pvm-debugger-adapter"; +import { RegistersArray } from "@/types/pvm.ts"; export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) => void }) => { const dispatch = useAppDispatch(); + const initialState = useAppSelector(selectInitialState); const [programLoad, setProgramLoad] = useState(); const [error, setError] = useState(); const [isSubmitted, setIsSubmitted] = useState(false); @@ -23,38 +27,92 @@ export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) = setError(""); }, [isLoading]); - const handleLoad = useCallback(async () => { - setIsSubmitted(true); - if (!programLoad) return; + const handleLoad = useCallback( + async (_event: unknown, program?: ProgramUploadFileOutput) => { + setIsSubmitted(true); - dispatch(setIsProgramEditMode(false)); + if (!programLoad && !program) return; - try { - await debuggerActions.handleProgramLoad(programLoad); - setIsDialogOpen?.(false); - } catch (error) { - if (error instanceof Error || isSerializedError(error)) { - setError(error.message); - } else { - setError("Unknown error occured"); + dispatch(setIsProgramEditMode(false)); + + try { + await debuggerActions.handleProgramLoad(program || programLoad); + setIsDialogOpen?.(false); + } catch (error) { + if (error instanceof Error || isSerializedError(error)) { + setError(error.message); + } else { + setError("Unknown error occurred"); + } + } + }, + [dispatch, programLoad, debuggerActions, setIsDialogOpen], + ); + + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search); + + if (searchParams.get("program")) { + const program = searchParams.get("program"); + + try { + // Add 0x prefix if it's not there - we're assuming it's the hex program either way + const hexProgram = program?.startsWith("0x") ? program : `0x${program}`; + const parsedBlob = bytes.BytesBlob.parseBlob(hexProgram ?? ""); + const parsedBlobArray = Array.prototype.slice.call(parsedBlob.raw); + + if (searchParams.get("flavour") === "jam") { + try { + const { code, /*memory,*/ registers } = decodeStandardProgram(parsedBlob.raw, new Uint8Array()); + + handleLoad({ + program: Array.from(code), + name: "custom", + initial: { + regs: Array.from(registers) as RegistersArray, + pc: 0, + pageMap: [], + // TODO: map memory properly + // memory: [...memory], + gas: 10000n, + }, + }); + } catch (e) { + console.warn("Could not load the program from URL", e); + } + } else { + handleLoad(undefined, { + program: parsedBlobArray, + name: "custom", + initial: initialState, + }); + } + + window.history.replaceState({}, document.title, "/"); + } catch (e) { + console.warn("Could not parse the program from URL", e); } } - }, [dispatch, programLoad, debuggerActions, setIsDialogOpen]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> - JSON tests - Examples - RAW bytecode + Upload file + Start with examples
- { setProgramLoad(val); setIsSubmitted(false); }} + onParseError={(error) => { + setError(error); + }} /> @@ -65,16 +123,7 @@ export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) = }} /> - - { - setProgramLoad(val); - setIsSubmitted(false); - setError(error); - }} - /> - - {error && isSubmitted &&

{error}

} + {error && isSubmitted &&

{error}

}