diff --git a/components/Editor/DownloadProof.tsx b/components/Editor/DownloadProof.tsx new file mode 100644 index 0000000..12b8c19 --- /dev/null +++ b/components/Editor/DownloadProof.tsx @@ -0,0 +1,24 @@ +export const DownloadProof = ({ proof }: { proof: string }) => { + function handleDownloadProof() { + const content = proof + const blob = new Blob([content], { type: 'application/json' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'proof.json' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + window.URL.revokeObjectURL(url) + } + + return ( + <> + Click{' '} + {' '} + to download the proof + + ) +} diff --git a/components/Editor/EditorControls.tsx b/components/Editor/EditorControls.tsx index 0b45757..544eddf 100644 --- a/components/Editor/EditorControls.tsx +++ b/components/Editor/EditorControls.tsx @@ -5,6 +5,7 @@ import { Priority, useRegisterActions } from 'kbar' import { OnChangeValue } from 'react-select' import { Button, Input } from 'components/ui' +import MultiButton from 'components/ui/MultiButton' import { cn } from '../../util/styles' @@ -23,7 +24,7 @@ type EditorControlsProps = { option: OnChangeValue, ) => void onCopyPermalink: () => void - onCompileRun: () => void + onCompileRun: (variant: 'run' | 'run-prove-verify') => void onProgramArgumentsUpdate: (args: string) => void onShowArgumentsHelper: () => void } @@ -48,7 +49,7 @@ const EditorControls = ({ keywords: 'compile run', section: 'Execution', perform: () => { - onCompileRun() + onCompileRun('run') }, subtitle: 'Run execution', priority: Priority.HIGH, @@ -119,8 +120,10 @@ const EditorControls = ({ })} /> -
- -
+ */} + {/* */} ) diff --git a/components/Editor/EditorFooter.tsx b/components/Editor/EditorFooter.tsx index fa74768..6b8a30d 100644 --- a/components/Editor/EditorFooter.tsx +++ b/components/Editor/EditorFooter.tsx @@ -16,7 +16,7 @@ function EditorFooter({ withoutContent = false }) { !isFullScreen && 'rounded-b-lg', )} > - Cairo Compiler v2.8.0 + Cairo Compiler v2.10.0 {isFullScreen && (
diff --git a/components/Editor/examples.ts b/components/Editor/examples.ts index 301fb13..5215966 100644 --- a/components/Editor/examples.ts +++ b/components/Editor/examples.ts @@ -4,6 +4,7 @@ export const Examples: ExampleCode = { Cairo: [ `use core::felt252; +#[executable] fn main() -> felt252 { let n = 2 + 3; n @@ -12,6 +13,7 @@ fn main() -> felt252 { const my_constant: felt252 = 42; +#[executable] fn main() { // non-mutable variable @@ -34,25 +36,27 @@ fn main() { // my_mut_var = 'hello world' <-- fails to compile }`, - `use core::felt252; - -fn main() { - let my_felt252 = 10; - - // Since a felt252 might not fit in a u8, we need to unwrap the Option type - let my_u8: u8 = my_felt252.try_into().unwrap(); - - let my_u16: u16 = my_u8.into(); - let my_u32: u32 = my_u16.into(); - let my_u64: u64 = my_u32.into(); - let _my_u128: u128 = my_u64.into(); - - // As a felt252 is smaller than a u256, we can use the into() method - let _my_u256: u256 = my_felt252.into(); - let _my_usize: usize = my_felt252.try_into().unwrap(); - let _my_other_felt252: felt252 = my_u8.into(); - let _my_third_felt252: felt252 = my_u16.into(); -}`, + // , + // `use core::felt252; + + // #[executable] + // fn main() { + // let my_felt252 = 10; + + // // Since a felt252 might not fit in a u8, we need to unwrap the Option type + // let my_u8: u8 = my_felt252.try_into().unwrap(); + + // let my_u16: u16 = my_u8.into(); + // let my_u32: u32 = my_u16.into(); + // let my_u64: u64 = my_u32.into(); + // let _my_u128: u128 = my_u64.into(); + + // // As a felt252 is smaller than a u256, we can use the into() method + // let _my_u256: u256 = my_felt252.into(); + // let _my_usize: usize = my_felt252.try_into().unwrap(); + // let _my_other_felt252: felt252 = my_u8.into(); + // let _my_third_felt252: felt252 = my_u16.into(); + // }` `#[derive(Drop)] enum Direction { Up, @@ -61,6 +65,7 @@ enum Direction { Right } +#[executable] fn main() { // if / else expression @@ -126,6 +131,7 @@ fn add(a: u32, b: u32) -> u64 { } // This functions doesn't return anything. +#[executable] fn main() { let a = 1; let b = 2; @@ -137,28 +143,30 @@ fn main() { let _z = add(a: x, b: y); } `, - `use array::ArrayTrait; + // `use array::ArrayTrait; -fn main () { - let mut a = ArrayTrait::new(); + // #[executable] + // fn main () { + // let mut a = ArrayTrait::new(); - // add some items in the array - a.append(1); - a.append(2); - - // get array length - assert!(a.len() == 2, "wrong array length"); + // // add some items in the array + // a.append(1); + // a.append(2); - // 2 ways to read an item from the array - // * get() returns an Option so you can handle out-of-bounds error - // * at() panics in case of out-of-bounds error - let first_element = *a.get(0).unwrap().unbox(); - // a.get(2) will return None + // // get array length + // assert!(a.len() == 2, "wrong array length"); - let second_element = *a.at(1); - // a.at(2) will cause an error -}`, - `fn main () { + // // 2 ways to read an item from the array + // // * get() returns an Option so you can handle out-of-bounds error + // // * at() panics in case of out-of-bounds error + // let first_element = *a.get(0).unwrap().unbox(); + // // a.get(2) will return None + + // let second_element = *a.at(1); + // // a.at(2) will cause an error + // }` + `#[executable] +fn main () { let mut balances: Felt252Dict = Default::default(); balances.insert('Alex', 100); @@ -182,6 +190,7 @@ fn foo_receives_ref(ref arr: Array) { // keeps the ownership of the array. } +#[executable] fn main() { // as the creator of arr, the main function owns the array let mut arr = ArrayTrait::::new(); @@ -197,6 +206,7 @@ fn main() { }`, `use core::felt252; +#[executable] fn main() -> felt252 { let n = 10; let result = fib(1, 1, n); @@ -494,10 +504,10 @@ ret; export const CairoExampleNames = [ 'Simple', 'Variables & mutability', - 'Type casting', + // 'Type casting', 'Control flow', 'Functions', - 'Arrays', + // 'Arrays', 'Dictionaries', 'Ownership', 'Fibonacci', diff --git a/components/Editor/index.tsx b/components/Editor/index.tsx index 535ceab..c1498b8 100644 --- a/components/Editor/index.tsx +++ b/components/Editor/index.tsx @@ -25,7 +25,12 @@ import { Setting, SettingsContext } from 'context/settingsContext' import { getAbsoluteURL } from 'util/browser' import { isArgumentStringValid } from 'util/compiler' -import { codeHighlight, isEmpty, objToQueryString } from 'util/string' +import { + codeHighlight, + formatTime, + isEmpty, + objToQueryString, +} from 'util/string' import { Examples } from 'components/Editor/examples' import { Tracer } from 'components/Tracer' @@ -36,11 +41,13 @@ import { cn } from '../../util/styles' import { ArgumentsHelperModal } from './ArgumentsHelperModal' import { registerCairoLanguageSupport } from './cairoLangConfig' import Console from './Console' +import { DownloadProof } from './DownloadProof' import EditorControls from './EditorControls' import EditorFooter from './EditorFooter' import ExtraColumn from './ExtraColumn' import Header from './Header' import { InstructionsTable } from './InstructionsTable' + // @ts-ignore - Cairo is not part of the official highlightjs package type Props = { readOnly?: boolean @@ -83,6 +90,10 @@ const Editor = ({ readOnly = false, isCairoLangPage = false }: Props) => { debugMode, activeSierraIndexes, setDebugMode, + proof, + proofTime, + verificationTime, + provingIsNotSupported, } = useContext(CairoVMApiContext) const { addToConsoleLog, isThreeColumnLayout } = useContext(AppUiContext) @@ -247,10 +258,34 @@ const Editor = ({ readOnly = false, isCairoLangPage = false }: Props) => { } if (executionState === ProgramExecutionState.Error) { + if (executionPanicMessage && executionPanicMessage.length > 0) { + addToConsoleLog( + 'Runtime error: ' + executionPanicMessage, + LogType.Error, + ) + } else { + addToConsoleLog('Runtime error', LogType.Error) + } + } + + if (proof && proofTime) { + addToConsoleLog('Generating proof...', LogType.Info) addToConsoleLog( - 'Runtime error: ' + executionPanicMessage, - LogType.Error, + `Proof generation successful (finished in ${formatTime(proofTime)})`, + LogType.Info, ) + addToConsoleLog(, LogType.Info) + if (verificationTime) { + addToConsoleLog('Verifying proof...', LogType.Info) + addToConsoleLog( + `Verification successful (finished in ${formatTime( + verificationTime, + )})`, + LogType.Info, + ) + } + } else if (provingIsNotSupported) { + addToConsoleLog('Proving is not supported for contracts', LogType.Error) } } else if (compilationState === ProgramCompilationState.CompilationErr) { addToConsoleLog('Compilation failed', LogType.Error) @@ -333,10 +368,18 @@ const Editor = ({ readOnly = false, isCairoLangPage = false }: Props) => { [setProgramArguments], ) - const handleCompileRun = useCallback(() => { - compileCairoCode(cairoCode, removeExtraWhitespaces(programArguments)) - setCompiledCairoCode(cairoCode) - }, [cairoCode, programArguments, compileCairoCode]) + const handleCompileRun = useCallback( + async (variant: 'run' | 'run-prove-verify' | 'prove') => { + await compileCairoCode( + cairoCode, + removeExtraWhitespaces(programArguments), + variant === 'run-prove-verify', + variant === 'run-prove-verify', + ) + setCompiledCairoCode(cairoCode) + }, + [compileCairoCode, cairoCode, programArguments], + ) const handleCopyPermalink = useCallback(() => { const params = { diff --git a/components/ui/MultiButton.tsx b/components/ui/MultiButton.tsx new file mode 100644 index 0000000..d5a1c7f --- /dev/null +++ b/components/ui/MultiButton.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react' + +import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' +import { ChevronDownIcon } from '@heroicons/react/20/solid' + +import { Button } from './Button' + +type CompileMods = 'run' | 'run-prove-verify' +interface MultiButtonProps { + onCompileRun: (variant: CompileMods) => void +} + +const MultiButton = ({ onCompileRun }: MultiButtonProps) => { + const [selected, setSelected] = useState('run-prove-verify') + + const handleMainButtonClick = () => { + switch (selected) { + case 'run-prove-verify': + onCompileRun('run-prove-verify') + break + case 'run': + onCompileRun('run') + break + default: + break + } + } + + return ( +
+ + + + + Open options + + +
+ + + + + + +
+
+
+
+ ) +} + +export default MultiButton diff --git a/context/appUiContext.tsx b/context/appUiContext.tsx index 47e21b4..6eb9bb0 100644 --- a/context/appUiContext.tsx +++ b/context/appUiContext.tsx @@ -1,4 +1,9 @@ -import React, { PropsWithChildren, createContext, useState } from 'react' +import React, { + PropsWithChildren, + ReactNode, + createContext, + useState, +} from 'react' export enum LogType { Error, @@ -14,7 +19,7 @@ export enum CodeType { export interface IConsoleOutput { type: LogType - message: string + message: string | ReactNode } type AppUiContextProps = { @@ -23,7 +28,7 @@ type AppUiContextProps = { consoleLog: IConsoleOutput[] toggleThreeColumnLayout: () => void toggleFullScreen: () => void - addToConsoleLog: (line: string, type?: LogType) => void + addToConsoleLog: (line: string | ReactNode, type?: LogType) => void enableFullScreen: () => void } @@ -59,7 +64,7 @@ export const AppUiProvider: React.FC = ({ children }) => { setIsThreeColumnLayout((prev) => !prev) } - const addToConsoleLog = (line: string, type = LogType.Info) => { + const addToConsoleLog = (line: string | ReactNode, type = LogType.Info) => { setConsoleLog((previous) => { const cloned = previous.map((x) => ({ ...x })) cloned.push({ type, message: line }) diff --git a/context/cairoVMApiContext.tsx b/context/cairoVMApiContext.tsx index a1dcd80..e1c7285 100644 --- a/context/cairoVMApiContext.tsx +++ b/context/cairoVMApiContext.tsx @@ -77,8 +77,19 @@ type ContextProps = { breakPoints?: BreakPoints sierraStatementsToCairoInfo?: SierraStatementsToCairoInfo + proof?: string + proofTime?: number + verificationTime?: number + + provingIsNotSupported: boolean + setDebugMode: (debugMode: ProgramDebugMode) => void - compileCairoCode: (cairoCode: string, programArguments: string) => void + compileCairoCode: ( + cairoCode: string, + programArguments: string, + isProofRequired: boolean, + isVerificationRequired: boolean, + ) => Promise onExecutionStepChange: (action: 'increase' | 'decrease') => void onContinueExecution: () => void addBreakPoint: (addr: string) => void @@ -107,7 +118,13 @@ export const CairoVMApiContext = createContext({ sierraStatementsToCairoInfo: {}, casmToSierraStatementsMap: {}, - compileCairoCode: noOp, + provingIsNotSupported: false, + + proof: undefined, + proofTime: undefined, + verificationTime: undefined, + + compileCairoCode: () => Promise.resolve(false), onExecutionStepChange: noOp, onContinueExecution: noOp, addBreakPoint: noOp, @@ -151,6 +168,12 @@ export const CairoVMApiProvider: React.FC = ({ useState({}) const [casmToSierraStatementsMap, setCasmToSierraStatementsMap] = useState({}) + const [proof, setProof] = useState(undefined) + const [proofTime, setProofTime] = useState(undefined) + const [verificationTime, setVerificationTime] = useState( + undefined, + ) + const [provingIsNotSupported, setProvingIsNotSupported] = useState(false) const currentTraceEntry = tracerData?.trace[executionTraceStepNumber] const currentSierraVariables = @@ -305,81 +328,95 @@ export const CairoVMApiProvider: React.FC = ({ setBreakPoints({ ...breakPoints, [addr]: false }) } - const compileCairoCode = (cairoCode: string, programArguments = '') => { + const compileCairoCode = async ( + cairoCode: string, + programArguments = '', + isProofRequired = false, + isVerificationRequired = false, + ) => { setCompilationState(ProgramCompilationState.Compiling) setExecutionState(ProgramExecutionState.Executing) - fetch(CAIRO_VM_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - cairo_program_code: cairoCode, - program_arguments: programArguments, - }), - }) - .then((response) => response.json()) - .then((data) => { - console.log('success') - - setCompilationState( - data.is_compilation_successful === true - ? ProgramCompilationState.CompilationSuccess - : ProgramCompilationState.CompilationErr, - ) - setSierraSubStepIndex(undefined) - setLogs(data.logs) - setExecutionState( - data.is_execution_successful === true - ? ProgramExecutionState.Success - : ProgramExecutionState.Error, - ) - setExecutionTraceStepNumber( - data.is_execution_successful === true - ? 0 - : data.tracer_data.trace.length - 2, - ) - setCasmCode(data.casm_program_code) - setSierraCode(data.sierra_program_code) - setCairoLangCompilerVersion(data.cairo_lang_compiler_version) - setSerializedOutput(data.serialized_output) - setExecutionPanicMessage(data.execution_panic_message) - setTracerData({ - memory: data.tracer_data.memory, - pcInstMap: data.tracer_data.pc_inst_map, - trace: data.tracer_data.trace, - callstack: data.tracer_data.callstack, - pcToInstIndexesMap: data.tracer_data.pc_to_inst_indexes_map, - entryToSierraVarsMap: data.tracer_data.trace_entries_to_sierra_vars, - }) - setBreakPoints( - Object.keys(data.tracer_data.memory).reduce( - (state, value) => ({ ...state, [value]: false }), - {}, - ), - ) - setSierraStatementsToCairoInfo( - data.tracer_data.sierra_to_cairo_debug_info - .sierra_statements_to_cairo_info, - ) - setCasmToSierraStatementsMap(data.casm_to_sierra_map) - setCasmInstructions( - parseStringInstructions(data.casm_formatted_instructions), - ) - const { sierraStatements, casmToSierraProgramMap } = - parseSierraFormattedProgramAndCasmToSierraMap( - data.sierra_formatted_program, - data.casm_to_sierra_map, - ) - setSierraStatements(sierraStatements) - setCasmToSierraProgramMap(casmToSierraProgramMap) + try { + const response = await fetch(CAIRO_VM_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + cairo_program_code: cairoCode, + program_arguments: programArguments, + proof_required: isProofRequired, + verification_required: isVerificationRequired, + }), }) - .catch((error) => { - console.log('error') - setCompilationState(ProgramCompilationState.CompilationErr) - console.error('Error:', error) + const responseContent = await response.text() + const data = JSON.parse(responseContent) + setCompilationState( + data.is_compilation_successful === true + ? ProgramCompilationState.CompilationSuccess + : ProgramCompilationState.CompilationErr, + ) + setSierraSubStepIndex(undefined) + setLogs(data.logs) + setExecutionState( + data.is_execution_successful === true + ? ProgramExecutionState.Success + : ProgramExecutionState.Error, + ) + if (!data.is_execution_successful) { + return false + } + setExecutionTraceStepNumber( + data.is_execution_successful === true + ? 0 + : data.tracer_data.trace.length - 2, + ) + setCasmCode(data.casm_program_code) + setSierraCode(data.sierra_program_code) + setCairoLangCompilerVersion(data.cairo_lang_compiler_version) + setSerializedOutput(data.serialized_output) + setExecutionPanicMessage(data.execution_panic_message) + setTracerData({ + memory: data.tracer_data.memory, + pcInstMap: data.tracer_data.pc_inst_map, + trace: data.tracer_data.trace, + callstack: data.tracer_data.callstack, + pcToInstIndexesMap: data.tracer_data.pc_to_inst_indexes_map, + entryToSierraVarsMap: data.tracer_data.trace_entries_to_sierra_vars, }) + setBreakPoints( + Object.keys(data.tracer_data.memory).reduce( + (state, value) => ({ ...state, [value]: false }), + {}, + ), + ) + setSierraStatementsToCairoInfo( + data.tracer_data.sierra_to_cairo_debug_info + .sierra_statements_to_cairo_info, + ) + setCasmToSierraStatementsMap(data.casm_to_sierra_map) + setCasmInstructions( + parseStringInstructions(data.casm_formatted_instructions), + ) + const { sierraStatements, casmToSierraProgramMap } = + parseSierraFormattedProgramAndCasmToSierraMap( + data.sierra_formatted_program, + data.casm_to_sierra_map, + ) + setSierraStatements(sierraStatements) + setCasmToSierraProgramMap(casmToSierraProgramMap) + setProof(data.proof ?? undefined) + setProofTime(data.proving_time_ms ?? undefined) + setVerificationTime(data.verification_time_ms ?? undefined) + setProvingIsNotSupported(data.proving_is_not_supported ?? false) + return true + } catch (error) { + console.log('error') + setCompilationState(ProgramCompilationState.CompilationErr) + console.error('Error:', error) + return false + } } return ( @@ -408,6 +445,10 @@ export const CairoVMApiProvider: React.FC = ({ casmToSierraStatementsMap, breakPoints, sierraStatementsToCairoInfo, + proof, + proofTime, + verificationTime, + provingIsNotSupported, setDebugMode, compileCairoCode, onExecutionStepChange, diff --git a/package-lock.json b/package-lock.json index aab2427..9ed1078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", "@kunigi/string-compression": "1.0.2", "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", @@ -2893,8 +2895,38 @@ "@floating-ui/utils": "^0.2.0" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, "node_modules/@goto-bus-stop/common-shake": { @@ -2953,6 +2985,34 @@ "xtend": "^4.0.2" } }, + "node_modules/@headlessui/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", + "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@tanstack/react-virtual": "^3.8.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "dev": true, @@ -3513,6 +3573,92 @@ "version": "1.2.0", "license": "MIT" }, + "node_modules/@react-aria/focus": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.1.tgz", + "integrity": "sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.23.0", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.23.0.tgz", + "integrity": "sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.27.0", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.27.0.tgz", + "integrity": "sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.27.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", + "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.27.0.tgz", + "integrity": "sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@remixicon/react": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.2.0.tgz", @@ -3616,6 +3762,33 @@ "devOptional": true, "license": "Apache-2.0" }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz", + "integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz", + "integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@ts-morph/common": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", @@ -12320,6 +12493,12 @@ "webpack": ">=2" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/table": { "version": "6.8.1", "dev": true, diff --git a/package.json b/package.json index 2d7dc73..ed3bc9f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@types/react-dom": "17.0.2" }, "dependencies": { + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", "@kunigi/string-compression": "1.0.2", "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", diff --git a/util/string.ts b/util/string.ts index a8c7baf..7569c13 100644 --- a/util/string.ts +++ b/util/string.ts @@ -86,3 +86,22 @@ export const objToQueryString = (params: any) => { .filter((param) => param !== null) .join('&') } + +/** + * Transforms a time value in milliseconds into a formatted string. + * - If the value is greater than 1000ms, converts it to seconds, + * rounds to 3 decimal places, and removes any trailing zeros. + * - Otherwise, returns the input in milliseconds. + * + * @param milliseconds - The time value in milliseconds. + * @returns A formatted string with the appropriate unit. + */ +export function formatTime(milliseconds: number): string { + if (milliseconds > 1000) { + const seconds = milliseconds / 1000 + // Use toFixed(3) to round to 3 decimals and then parseFloat to remove trailing zeros. + return `${parseFloat(seconds.toFixed(3))}s` + } else { + return `${milliseconds}ms` + } +}