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 (
+
+
+
+
+
+ )
+}
+
+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`
+ }
+}