From 928809dc6e6370588e545bae0a72169fa3cb562e Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 25 Feb 2026 11:18:26 -0500 Subject: [PATCH 01/31] block execution states to show IO --- recursive_blocks/src/Block.tsx | 11 +++++++++++ recursive_blocks/src/BlockConfig.ts | 2 +- recursive_blocks/src/BlockEditor.tsx | 14 +++++++++++--- recursive_blocks/src/BlockEditorContext.tsx | 13 ++++++++++++- recursive_blocks/src/BlockUtil.tsx | 9 +++++++-- recursive_blocks/src/Toolbar.tsx | 13 +++++++++++-- 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 5380458..31296be 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -5,6 +5,7 @@ import './Block.css'; import { blockConfig, BlockSlot, BlockType } from "./BlockConfig"; import { ValueEditor } from "./ValueEditor"; import { BlockSlotDisplay } from "./BlockSlot"; +import { useBlockEditor } from "./BlockEditorContext"; interface Props { block: BlockData | null; @@ -30,6 +31,7 @@ and replaces the old block with the new block. export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock }: Props) { const [collapsed, setCollapsed] = React.useState(block?.collapsed); const [showInfo, setShowInfo] = React.useState(false); + const { blockExecutionStates } = useBlockEditor(); if (!block) { return Drop block here; @@ -51,6 +53,8 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on const dragRef = React.useRef(null); drag(dragRef); + const executionState = blockExecutionStates[block.id]; + return (
+ {executionState && (executionState.inputs !== undefined || executionState.output !== undefined) && ( +
+ In: {executionState.inputs !== undefined ? `[${executionState.inputs.join(', ')}]` : '-'} + Out: {executionState.output !== undefined ? executionState.output : '-'} +
+ )} + {showInfo && blockConfig[block.type]?.description && (
{blockConfig[block.type]?.description ?? "No description available for this block."} diff --git a/recursive_blocks/src/BlockConfig.ts b/recursive_blocks/src/BlockConfig.ts index 729d258..a68b82b 100644 --- a/recursive_blocks/src/BlockConfig.ts +++ b/recursive_blocks/src/BlockConfig.ts @@ -6,7 +6,7 @@ export type BlockType = "Zero" | "Successor" | "Projection" | "Composition" | "P // Function signature for an evaluation function. // It takes in the block to evaluate, the inputs, and the evaluateBlock function, and returns an output number. // onStepCallback is optional and used for stepping through evaluation. -export type BlockEvaluator = (block: BlockData, inputs: number[], evaluate: BlockEvaluator, onStepCallback?: (block: BlockData, result: number) => Promise) => number | Promise; +export type BlockEvaluator = (block: BlockData, inputs: number[], evaluate: BlockEvaluator, onStepCallback?: (block: BlockData, result: number | null, inputs: number[]) => Promise) => number | Promise; // Function signature for a function that takes in a number, and returns a string to be displayed in a blockslot wanting that many inputs. export type InputDescriptorGenerator = (inputCount: number) => string; diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 68d9b1a..157f475 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -25,7 +25,7 @@ export const customBlocks: Record = {}; // JSX element that represents the editor, containing a root block and the header. export function BlockEditor() { - const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount } = useBlockEditor(); + const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates } = useBlockEditor(); const [inputs, setInputs] = useState(new Array(inputCount > 0 ? inputCount : 0).fill(0)); const [highlightedBlockId, setHighlightedBlockId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); @@ -288,6 +288,7 @@ export function BlockEditor() { setIsEvaluating(true); isHaltedRef.current = false; setCurrentResult(null); + setBlockExecutionStates({}); // Clear previous states ignoreBreakpointsRef.current = ignoreBreakpoints; if (!rootBlock) { @@ -305,12 +306,19 @@ export function BlockEditor() { const delay = speedMap[evaluationSpeed]; // the callback function to be run after evaluation - const onStepCallback = async (block: BlockData, result: number) => { + const onStepCallback = async (block: BlockData, result: number | null, inputs: number[]) => { if (isHaltedRef.current) throw new Error("Halted"); // update UI setHighlightedBlockId(block.id); - setCurrentResult(result); + if (result !== null) { + setCurrentResult(result); + } + + setBlockExecutionStates(prev => ({ + ...prev, + [block.id]: { inputs, output: result !== null ? result : undefined } + })); // control flow: pause execution if there is a breakpoint or we are stepping const shouldPause = (breakpointsRef.current.has(block.id) && !ignoreBreakpointsRef.current) || pauseAtNextStepRef.current; diff --git a/recursive_blocks/src/BlockEditorContext.tsx b/recursive_blocks/src/BlockEditorContext.tsx index e53ba68..5c0c1b3 100644 --- a/recursive_blocks/src/BlockEditorContext.tsx +++ b/recursive_blocks/src/BlockEditorContext.tsx @@ -6,21 +6,32 @@ import { DEFAULT_INPUT_COUNT } from "./BlockEditor"; Defines a React Context, which makes the variables inside accessable by any React element within the context. */ +export type ExecutionState = { inputs?: number[]; output?: number }; + const BlockEditorContext = createContext<{ rootBlock: BlockData | null; customBlockCount: number; inputCount: number; + blockExecutionStates: Record; setInputCount: React.Dispatch>; setRootBlock: React.Dispatch>; setCustomBlockCount: React.Dispatch>; + setBlockExecutionStates: React.Dispatch>>; } | null>(null); export function BlockEditorProvider({ children }: { children: React.ReactNode }) { const [rootBlock, setRootBlock] = useState(null); const [inputCount, setInputCount] = useState(DEFAULT_INPUT_COUNT); const [customBlockCount, setCustomBlockCount] = useState(0); + const [blockExecutionStates, setBlockExecutionStates] = useState>({}); + return ( - + {children} ); diff --git a/recursive_blocks/src/BlockUtil.tsx b/recursive_blocks/src/BlockUtil.tsx index fc50eb1..f500756 100644 --- a/recursive_blocks/src/BlockUtil.tsx +++ b/recursive_blocks/src/BlockUtil.tsx @@ -61,16 +61,21 @@ export async function evaluateBlock( export async function stepBlock( block: BlockData, inputs: number[], - onStepCallback?: (block: BlockData, result: number) => Promise + onStepCallback?: (block: BlockData, result: number | null, inputs: number[]) => Promise ): Promise { const evaluateWithCallback: BlockEvaluator = async (b, i, _eval, callback) => { return await stepBlock(b, i, callback); }; + if (onStepCallback) { + await onStepCallback(block, null, inputs); + } + const config = blockConfig[block.type]; const ev = await config.evaluate(block, inputs, evaluateWithCallback, onStepCallback); + if (onStepCallback) { - await onStepCallback(block, ev); + await onStepCallback(block, ev, inputs); } return ev; } diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index 47e844a..51b0530 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -6,6 +6,9 @@ interface ToolbarProps { onLoad: (event: React.ChangeEvent) => void; loadInputRef: React.RefObject; onRun: () => void; + onRunIgnoreBreakpoints: () => void; + onResume: () => void; + paused: boolean; onHalt: () => void; onStep: () => void; inputCount: number; @@ -24,6 +27,9 @@ export function Toolbar({ onLoad, loadInputRef, onRun, + onRunIgnoreBreakpoints, + onResume, + paused, onHalt, onStep, inputCount, @@ -69,8 +75,11 @@ export function Toolbar({ {/* Program operation buttons */}
- + + From 5a5f441b48eb5bed80e375234a2019857b36d7eb Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 25 Feb 2026 12:56:11 -0500 Subject: [PATCH 03/31] refactor control flow --- recursive_blocks/src/BlockEditor.tsx | 168 +++++++++++---------------- 1 file changed, 70 insertions(+), 98 deletions(-) diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 5dbc483..e64984b 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -23,6 +23,12 @@ export const DEFAULT_INPUT_COUNT = 2; export const customBlocks: Record = {}; +enum StepMode { + None, + Step, + Trace +} + // JSX element that represents the editor, containing a root block and the header. export function BlockEditor() { const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates } = useBlockEditor(); @@ -37,8 +43,7 @@ export function BlockEditor() { const loadInputRef = useRef(null); const pauseResolver = useRef<((value: void | PromiseLike) => void) | null>(null); const isHaltedRef = useRef(false); - const pauseAtNextStepRef = useRef(false); - const traceAtNextStepRef = useRef(false); + const stepModeRef = useRef(StepMode.None); const breakpointsRef = useRef>(new Set()); const ignoreBreakpointsRef = useRef(false); @@ -242,124 +247,86 @@ export function BlockEditor() { const handleHalt = () => { isHaltedRef.current = true; - pauseAtNextStepRef.current = false; - traceAtNextStepRef.current = false; + stepModeRef.current = StepMode.None; - // resolve any pending pause so execution can proceed to throw "halted" if (pauseResolver.current) { pauseResolver.current(); pauseResolver.current = null; } setPaused(false); }; - - const handleResume = () => { - if (pauseResolver.current) { - pauseResolver.current(); - pauseResolver.current = null; - setPaused(false); - } - }; - - const advanceExecution = (isTrace: boolean) => { - pauseAtNextStepRef.current = true; - traceAtNextStepRef.current = isTrace; - - if (isEvaluating && paused) { - handleResume(); - } else { - handleRun(); - } - }; - const handleStep = async () => advanceExecution(false); - const handleTrace = async () => advanceExecution(true); + const startOrResume = async (mode: StepMode, ignoreBreakpoints: boolean = false) => { + stepModeRef.current = mode; + ignoreBreakpointsRef.current = ignoreBreakpoints; - const handleRun = async (ignoreBreakpoints: boolean = false) => { - // if paused, handle the "Run (Ignore Breakpoints)" case - if (paused && ignoreBreakpoints) { - ignoreBreakpointsRef.current = true; - handleResume(); + if (isEvaluating) { + if (paused && pauseResolver.current) { + pauseResolver.current(); + pauseResolver.current = null; + setPaused(false); + } return; } - // if not paused, continue as usual - if (isEvaluating) return; - setIsEvaluating(true); - isHaltedRef.current = false; - setCurrentResult(null); - setBlockExecutionStates({}); // Clear previous states - ignoreBreakpointsRef.current = ignoreBreakpoints; - if (!rootBlock) { alert("No root block to evaluate."); - setIsEvaluating(false); return; } - try { - const speedMap: Record = { - 0: 500, - 1: 100, - 2: 0, - }; - const delay = speedMap[evaluationSpeed]; - - // the callback function to be run after evaluation - const onStepCallback = async (block: BlockData, result: number | null, inputs: number[]) => { - if (isHaltedRef.current) throw new Error("Halted"); - - const isBeforeEval = result === null; - - // update UI - setHighlightedBlockId(block.id); - if (result !== null) { - setCurrentResult(result); - } + setIsEvaluating(true); + isHaltedRef.current = false; + setCurrentResult(null); + setBlockExecutionStates({}); - setBlockExecutionStates(prev => ({ - ...prev, - [block.id]: { inputs, output: result !== null ? result : undefined } - })); + const speedMap: Record = { 0: 500, 1: 100, 2: 0 }; + const delay = speedMap[evaluationSpeed]; - let shouldPause = false; + // the callback function to be run after evaluation + const onStepCallback = async (block: BlockData, result: number | null, inputs: number[]) => { + if (isHaltedRef.current) throw new Error("Halted"); - // breakpoints pause on before eval - if (breakpointsRef.current.has(block.id) && !ignoreBreakpointsRef.current && isBeforeEval) { - shouldPause = true; - } + const isBeforeEval = result === null; - // stepping / tracing pause logic - if (pauseAtNextStepRef.current) { - if (traceAtNextStepRef.current) { - shouldPause = true; - } else if (!isBeforeEval) { - shouldPause = true; - } - } + // update this block's UI + setHighlightedBlockId(block.id); + if (!isBeforeEval) setCurrentResult(result); - if (shouldPause) { - setPaused(true); - pauseAtNextStepRef.current = false; - traceAtNextStepRef.current = false; - await new Promise(resolve => { - pauseResolver.current = resolve; - }); - setPaused(false); - } + // update this block's execution state + setBlockExecutionStates(prev => ({ + ...prev, + [block.id]: { inputs, output: !isBeforeEval ? result : undefined } + })); - // wait to simulate execution speed - if (delay > 0 && !isBeforeEval) { - await sleep(delay); - } - }; - // stepBlock will run evaluation, then handle the callback + // pause if a breakpoint is hit, or we are stepping or tracing + let shouldPause = false; + if (breakpointsRef.current.has(block.id) && !ignoreBreakpointsRef.current && isBeforeEval) { + shouldPause = true; + } + if (stepModeRef.current === StepMode.Step && !isBeforeEval) { + shouldPause = true; + } + if (stepModeRef.current === StepMode.Trace) { + shouldPause = true; + } + + if (shouldPause) { + setPaused(true); + stepModeRef.current = StepMode.None; + await new Promise(resolve => { pauseResolver.current = resolve; }); + } + + if (delay > 0 && !isBeforeEval) { + await sleep(delay); + } + }; + + // execute stepBlock + try { await stepBlock(rootBlock, inputs, onStepCallback); } catch (error) { - if (error instanceof Error) { - if (error.message !== "Halted") { - alert(`Error: ${error.message}`); - } + if (error instanceof Error && error.message !== "Halted") { + alert(`Error: ${error.message}`); } } finally { setHighlightedBlockId(null); @@ -369,6 +336,11 @@ export function BlockEditor() { } }; + const handleRun = () => startOrResume(StepMode.None, false); + const handleRunIgnoreBreakpoints = () => startOrResume(StepMode.None, true); + const handleStep = () => startOrResume(StepMode.Step, false); + const handleTrace = () => startOrResume(StepMode.Trace, false); + const speedToText = (speed: number) => { if (speed === 0) return "Slow"; if (speed === 1) return "Fast"; @@ -383,9 +355,9 @@ export function BlockEditor() { loadInputRef={loadInputRef} - onRun={() => handleRun(false)} - onRunIgnoreBreakpoints={() => handleRun(true)} - onResume={handleResume} + onRun={handleRun} + onRunIgnoreBreakpoints={handleRunIgnoreBreakpoints} + onResume={handleRun} paused={paused} onHalt={handleHalt} onStep={handleStep} From 186aa1ba7e162caad0d8aef33a9b8e80860ff95c Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 25 Feb 2026 14:55:37 -0500 Subject: [PATCH 04/31] fix resume after breakpoint stuttering bug --- recursive_blocks/src/BlockEditor.tsx | 23 ++++++++++++++++++----- recursive_blocks/src/Toolbar.tsx | 6 +++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index e64984b..1e23545 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -44,8 +44,9 @@ export function BlockEditor() { const pauseResolver = useRef<((value: void | PromiseLike) => void) | null>(null); const isHaltedRef = useRef(false); const stepModeRef = useRef(StepMode.None); - const breakpointsRef = useRef>(new Set()); const ignoreBreakpointsRef = useRef(false); + const breakpointsRef = useRef>(new Set()); + const selfRecDepthRef = useRef>(new Map()); // Every time the root block changes, rebuild the set of breakpoints (this keeps it dynamic) useEffect(() => { @@ -256,7 +257,7 @@ export function BlockEditor() { setPaused(false); }; - const startOrResume = async (mode: StepMode, ignoreBreakpoints: boolean = false) => { + const startOrResume = async (mode: StepMode, ignoreBreakpoints: boolean) => { stepModeRef.current = mode; ignoreBreakpointsRef.current = ignoreBreakpoints; @@ -278,6 +279,7 @@ export function BlockEditor() { isHaltedRef.current = false; setCurrentResult(null); setBlockExecutionStates({}); + selfRecDepthRef.current.clear(); const speedMap: Record = { 0: 500, 1: 100, 2: 0 }; const delay = speedMap[evaluationSpeed]; @@ -298,11 +300,22 @@ export function BlockEditor() { [block.id]: { inputs, output: !isBeforeEval ? result : undefined } })); - // pause if a breakpoint is hit, or we are stepping or tracing let shouldPause = false; - if (breakpointsRef.current.has(block.id) && !ignoreBreakpointsRef.current && isBeforeEval) { - shouldPause = true; + + // track self-recursion depth so that breakpoints only pause on the first (outermost) call + if (breakpointsRef.current.has(block.id)) { + const currentDepth = selfRecDepthRef.current.get(block.id) || 0; + if (isBeforeEval) { + selfRecDepthRef.current.set(block.id, currentDepth + 1); + // pause only if this is the first time we're entering this block + if (currentDepth === 0 && !ignoreBreakpointsRef.current) { + shouldPause = true; + } + } else { + selfRecDepthRef.current.set(block.id, Math.max(0, currentDepth - 1)); + } } + if (stepModeRef.current === StepMode.Step && !isBeforeEval) { shouldPause = true; } diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index 2643974..1c347fd 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -81,13 +81,13 @@ export function Toolbar({ ▶️ {paused ? "Resume" : "Run"} - + +
In: - {formatInput(block.latestInput)} + {formatInput(executionState?.inputs ?? block.latestInput)}
@@ -181,13 +181,6 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on - {/*executionState && (executionState.inputs !== undefined || executionState.output !== undefined) && ( -
- In: {executionState.inputs !== undefined ? `[${executionState.inputs.join(', ')}]` : '-'} - Out: {executionState.output !== undefined ? executionState.output : '-'} -
- )*/} - {showInfo && blockConfig[block.type]?.description && (
{blockConfig[block.type]?.description ?? "No description available for this block."} @@ -204,7 +197,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
Out: - {formatOutput(block.latestOutput)} + {formatOutput(executionState?.output ?? block.latestOutput)}
); diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 1c2efa0..f75fcf9 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -9,8 +9,7 @@ import { BlockPalette } from "./BlockPalette"; import { removeBlockById } from "./BlockUtil"; // TEMP: Import test helper functions -//import { addGarbageTestData, clearExecutionData } from "./TEST_HELPER"; - +import { addGarbageTestData, clearExecutionData } from "./TEST_HELPER"; export interface EditorSaveState { fileType: string; @@ -126,7 +125,7 @@ export function BlockEditor() { setSelectedBlockId(null); setHighlightedBlockId(null); }, [rootBlock, selectedBlockId]); -/* + const handleAddTestData = useCallback(() => { if (!rootBlock) { alert("No blocks to add test data to. Create some blocks first!"); @@ -143,7 +142,7 @@ export function BlockEditor() { setRootBlock({ ...rootBlock }); console.log("Test data cleared from all blocks"); }, [rootBlock, setRootBlock]); -*/ + useEffect(() => { function handleKeyDown(e: KeyboardEvent) { const isMac = navigator.platform.toUpperCase().includes("MAC"); @@ -363,7 +362,7 @@ export function BlockEditor() { }; const handleRun = () => startOrResume(StepMode.None, false); - const handleRunIgnoreBreakpoints = () => startOrResume(StepMode.None, true); + const handleForceRun = () => startOrResume(StepMode.None, true); const handleStep = () => startOrResume(StepMode.Step, false); const handleTrace = () => startOrResume(StepMode.Trace, false); @@ -381,9 +380,8 @@ export function BlockEditor() { loadInputRef={loadInputRef} onRun={handleRun} - onRunIgnoreBreakpoints={handleRunIgnoreBreakpoints} - onResume={handleRun} - paused={paused} + onForceRun={handleForceRun} + //paused={paused} onHalt={handleHalt} onStep={handleStep} onTrace={handleTrace} diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index 5f3a3be..e02c7a8 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -8,7 +8,7 @@ interface ToolbarProps { onRun: () => void; onForceRun: () => void; onStep: () => void; - onTrace?: () => void; + onTrace: () => void; onHalt: () => void; inputCount: number; onInputCountChange: (count: number) => void; From 21117e3c72b20d6c57901aa3a9d52edb9d013e61 Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 11 Mar 2026 12:56:44 -0400 Subject: [PATCH 07/31] minor css changes --- recursive_blocks/src/Block.css | 27 ++++++++++++++++----------- recursive_blocks/src/Block.tsx | 9 +++++---- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 3beed79..c88323c 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -111,13 +111,13 @@ .block-math-notation { font-size: 0.72rem; font-style: italic; - color: rgba(0,0,0,0.5); + color: rgba(0,0,0,0.75); margin-top: 1px; font-family: Georgia, 'Times New Roman', serif; letter-spacing: 0.01em; } -.block-highlighted .block-math-notation { +.block-highlighted > .block-header .block-math-notation { color: rgba(255,255,255,0.45); } @@ -130,38 +130,41 @@ display: flex; align-items: center; gap: 0.35rem; - font-family: inherit; + width: max-content; + font-family: monospace; font-size: 0.75rem; padding-left: 0.2rem; + /*background-color: rgba(255,255,255,0.6);*/ } .block-io-out { display: flex; align-items: center; gap: 0.35rem; - font-family: inherit; + width: max-content; + font-family: monospace; font-size: 0.75rem; margin-top: 0.5rem; - padding-top: 0.4rem; - border-top: 1px solid rgba(0,0,0,0.12); + padding: 0.2rem 0.4rem; + /*background-color: rgba(255,255,255,0.6);*/ } .block-io-label { font-weight: 600; - color: rgba(255,255,255,0.95) !important; + color: rgba(0,0,0,0.95) !important; font-size: 0.75rem; letter-spacing: 0.02em; } .block-io-value { font-weight: 700; - color: rgba(255,255,255,0.95) !important; + color: rgba(0,0,0,0.95) !important; min-width: 1.5rem; text-align: center; } .block-io-separator { - color: rgba(255,255,255,0.6) !important; + color: rgba(0,0,0,0.6) !important; font-weight: 400; } @@ -188,14 +191,14 @@ letter-spacing: 0.03em; } -.collapse-button { color: rgba(255,255,255,0.5); } +.collapse-button { color: rgba(0,0,0,0.4); } .collapse-button:hover { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.85); } .info-button { - color: rgba(255,255,255,0.5); + color: rgba(0,0,0,0.4); font-family: Georgia, 'Times New Roman', serif; font-style: italic; font-size: 0.85rem; @@ -285,6 +288,7 @@ /* Value Editor */ .value-editor { margin-top: 0.75rem; + padding-left: 0.2rem; padding-right: 1rem; } @@ -302,6 +306,7 @@ align-items: center; gap: 0.75rem; margin-bottom: 0.625rem; + color: rgba(0,0,0,0.95); } .value-label { diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 3d6feed..59fad52 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -138,10 +138,6 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on )} -
- In: - {formatInput(executionState?.inputs ?? block.latestInput)} -
@@ -187,6 +183,11 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
)} +
+ In: + {formatInput(executionState?.inputs ?? block.latestInput)} +
+
{block.children.map((slot) => (
From 44b9e529f22f7447a3a592ddc2fda15d17a82522 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Wed, 18 Mar 2026 02:08:05 -0400 Subject: [PATCH 08/31] CSS fixes renamed projection, changed = to -->, collapsable fields, edit button --- recursive_blocks/src/Block.tsx | 23 ++++++------ recursive_blocks/src/BlockConfig.ts | 4 ++- recursive_blocks/src/BlockEditor.tsx | 7 ---- recursive_blocks/src/BlockEditorContext.tsx | 6 +++- recursive_blocks/src/BlockPalette.tsx | 3 +- recursive_blocks/src/TEST_HELPER.tsx | 35 ------------------ recursive_blocks/src/Toolbar.css | 18 ++++++++++ recursive_blocks/src/Toolbar.tsx | 39 +++++++++------------ recursive_blocks/src/ValueEditor.tsx | 6 ++-- 9 files changed, 62 insertions(+), 79 deletions(-) delete mode 100644 recursive_blocks/src/TEST_HELPER.tsx diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 59fad52..92c45f5 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -63,7 +63,7 @@ onUpdate is a function that gets called when this block is modified and replaces the old block with the new block. */ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock, isRunning = false }: Props) { - const [collapsed, setCollapsed] = React.useState(block?.collapsed); + const [collapsed, setCollapsed] = React.useState(block?.collapsed ?? false); const [showInfo, setShowInfo] = React.useState(false); const { blockExecutionStates } = useBlockEditor(); @@ -132,7 +132,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on {block.hasBreakpoint ? "\u25CF" : "\u25CB"}
-
{block.name || block.type.toUpperCase()}
+
{block.name || blockConfig[block.type]?.displayName || block.type.toUpperCase()}
{block.type !== "Custom" && (
{getMathNotation(block)}
)} @@ -155,10 +155,10 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on title={"Show description"} onClick={() => setShowInfo(prev => !prev)} > - i + {block.type === "Projection" ? "→" : "i"} )} - {block.children.length > 0 && ( + {(block.children.length > 0 || (block.num_values && block.num_values.length > 0)) && (
-
- {block.children.map((slot) => ( -
- ))} -
+ {/* hide slots and ValueEditor when collapsed */} + {!collapsed && ( +
+ {block.children.map((slot) => ( +
+ ))} +
+ )} - + {!collapsed && }
Out: diff --git a/recursive_blocks/src/BlockConfig.ts b/recursive_blocks/src/BlockConfig.ts index a68b82b..71e0eb0 100644 --- a/recursive_blocks/src/BlockConfig.ts +++ b/recursive_blocks/src/BlockConfig.ts @@ -70,6 +70,7 @@ export const blockConfig: Record string[]; + displayName?: string; description?: string; }> = { "Zero": { @@ -109,6 +110,7 @@ export const blockConfig: Record { diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index f75fcf9..d7b5b46 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -8,9 +8,6 @@ import { BlockSlotDisplay } from "./BlockSlot"; import { BlockPalette } from "./BlockPalette"; import { removeBlockById } from "./BlockUtil"; -// TEMP: Import test helper functions -import { addGarbageTestData, clearExecutionData } from "./TEST_HELPER"; - export interface EditorSaveState { fileType: string; rootBlock?: BlockSave; @@ -131,14 +128,12 @@ export function BlockEditor() { alert("No blocks to add test data to. Create some blocks first!"); return; } - addGarbageTestData(rootBlock); setRootBlock({ ...rootBlock }); console.log("Test data added to all blocks"); }, [rootBlock, setRootBlock]); const handleClearTestData = useCallback(() => { if (!rootBlock) return; - clearExecutionData(rootBlock); setRootBlock({ ...rootBlock }); console.log("Test data cleared from all blocks"); }, [rootBlock, setRootBlock]); @@ -393,8 +388,6 @@ export function BlockEditor() { onEvaluationSpeedChange={setEvaluationSpeed} speedToText={speedToText} currentResult={currentResult} - onAddTestData={handleAddTestData} - onClearTestData={handleClearTestData} />
diff --git a/recursive_blocks/src/BlockEditorContext.tsx b/recursive_blocks/src/BlockEditorContext.tsx index 5c0c1b3..3bea7c5 100644 --- a/recursive_blocks/src/BlockEditorContext.tsx +++ b/recursive_blocks/src/BlockEditorContext.tsx @@ -17,6 +17,8 @@ const BlockEditorContext = createContext<{ setRootBlock: React.Dispatch>; setCustomBlockCount: React.Dispatch>; setBlockExecutionStates: React.Dispatch>>; + editMode: boolean; + setEditMode: React.Dispatch>; } | null>(null); export function BlockEditorProvider({ children }: { children: React.ReactNode }) { @@ -24,13 +26,15 @@ export function BlockEditorProvider({ children }: { children: React.ReactNode }) const [inputCount, setInputCount] = useState(DEFAULT_INPUT_COUNT); const [customBlockCount, setCustomBlockCount] = useState(0); const [blockExecutionStates, setBlockExecutionStates] = useState>({}); + const [editMode, setEditMode] = useState(false); return ( {children} diff --git a/recursive_blocks/src/BlockPalette.tsx b/recursive_blocks/src/BlockPalette.tsx index dfbc4fb..a5e79a1 100644 --- a/recursive_blocks/src/BlockPalette.tsx +++ b/recursive_blocks/src/BlockPalette.tsx @@ -34,7 +34,8 @@ function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType ref={ref} className={`palette-block ${isDragging ? "opacity-50" : "palette-block-appear"}`} > -
{custom_block_name !== undefined ? custom_block_name : type.toUpperCase()}
+ {/* use displayName from blockConfig if available */} +
{custom_block_name !== undefined ? custom_block_name : (blockConfig[type]?.displayName ?? type.toUpperCase())}
{showInfo && blockConfig[type].description && (
diff --git a/recursive_blocks/src/TEST_HELPER.tsx b/recursive_blocks/src/TEST_HELPER.tsx deleted file mode 100644 index 951ba3d..0000000 --- a/recursive_blocks/src/TEST_HELPER.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// TEST_HELPER.tsx -// This file shows how to add garbage test data to blocks for testing the In/Out display -import { BlockData } from "./BlockUtil"; - -// Adds random garbage input/output values to a block and all its children for testing the visualization -export function addGarbageTestData(block: BlockData): void { - - // Generate random garbage data - const inputCount = block.inputCount || 2; - const garbageInput = Array.from({ length: inputCount }, () => Math.floor(Math.random() * 10)); - const garbageOutput = Math.floor(Math.random() * 20); - - // Add to this block - block.latestInput = garbageInput; - block.latestOutput = garbageOutput; - - // Recursively add to children - for (const slot of block.children) { - if (slot.block) { - addGarbageTestData(slot.block); - } - } -} - -// Clears all input/output data from a block and its children -export function clearExecutionData(block: BlockData): void { - block.latestInput = undefined; - block.latestOutput = undefined; - - for (const slot of block.children) { - if (slot.block) { - clearExecutionData(slot.block); - } - } -} \ No newline at end of file diff --git a/recursive_blocks/src/Toolbar.css b/recursive_blocks/src/Toolbar.css index d51db82..1808f3c 100644 --- a/recursive_blocks/src/Toolbar.css +++ b/recursive_blocks/src/Toolbar.css @@ -115,6 +115,24 @@ box-shadow: none; } +/* disabled state for Edit button during execution */ +.toolbar-button-disabled, +.toolbar-button-disabled:hover { + opacity: 0.35; + cursor: not-allowed; + border-color: transparent !important; +} +.toolbar-button-active { + border-color: var(--contrast-col) !important; + color: var(--contrast-col) !important; + border-radius: 4px !important; +} + +.toolbar-button-active:hover { + border-color: var(--contrast-col) !important; + border-radius: 4px !important; +} + /* Icon Buttons */ .icon-button { width: 38px; diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index e02c7a8..acc89a5 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -1,5 +1,6 @@ import React, { } from "react"; import './Toolbar.css'; +import { useBlockEditor } from "./BlockEditorContext"; interface ToolbarProps { onSave: () => void; @@ -18,8 +19,6 @@ interface ToolbarProps { onEvaluationSpeedChange: (speed: number) => void; speedToText: (speed: number) => string; currentResult: number | null; - onAddTestData?: () => void; - onClearTestData?: () => void; } export function Toolbar({ @@ -39,9 +38,9 @@ export function Toolbar({ onEvaluationSpeedChange, speedToText, currentResult, - onAddTestData, - onClearTestData }: ToolbarProps) { + const { editMode, setEditMode } = useBlockEditor(); + const [executionLocked, setExecutionLocked] = React.useState(false); return ( <> @@ -72,49 +71,45 @@ export function Toolbar({
+ {/* Edit mode toggle button */}
- -
{/* Program execution controls */} -
- - - - - )} {(block.children.length > 0 || (block.num_values && block.num_values.length > 0)) && ( @@ -190,10 +190,35 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on {/* hide slots and ValueEditor when collapsed */} {!collapsed && ( -
- {block.children.map((slot) => ( -
- ))} +
+ {block.children.map((slot) => { + const slotDisplay = ( + + ); + + if (block.type === "Composition") { + const label = slot.name.startsWith("g") + ? g{slot.name.slice(1)} + : f; + + return ( +
+
{label}:
+
{slotDisplay}
+
+ ); + } + + return
{slotDisplay}
; + })}
)} diff --git a/recursive_blocks/src/BlockConfig.ts b/recursive_blocks/src/BlockConfig.ts index 71e0eb0..b69f12b 100644 --- a/recursive_blocks/src/BlockConfig.ts +++ b/recursive_blocks/src/BlockConfig.ts @@ -161,13 +161,16 @@ export const blockConfig: Record c.name === "f")?.block; + if (!f_block) { + throw new Error(`f block is missing in Composition.`); + } + const result = await evaluate(f_block, g_results, evaluate, onStepCallback); return result; }, dynamicChildren: (block: BlockData) => { const m = block!.num_values!.find(v => v.name === "m")?.value ?? 1; return [ - { name: "f", block: block.children.find(c => c.name === "f")?.block ?? null, input_descriptor_index: 1, input_set: m }, ...Array.from({ length: m }, (_, i) => { const name = `g${i + 1}`; return { @@ -175,7 +178,8 @@ export const blockConfig: Record c.name === name)?.block ?? null, input_descriptor_index: 0, }; - }) + }), + { name: "f", block: block.children.find(c => c.name === "f")?.block ?? null, input_descriptor_index: 1, input_set: m } ]; }, checkForErrors: (block) => { From 381182d56fa6661869b6eef071e99082b7b85321 Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 18 Mar 2026 10:59:04 -0400 Subject: [PATCH 10/31] fix edit mode bugs --- recursive_blocks/src/BlockEditor.tsx | 3 ++- recursive_blocks/src/Toolbar.tsx | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index d7b5b46..1535517 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -30,7 +30,7 @@ enum StepMode { // JSX element that represents the editor, containing a root block and the header. export function BlockEditor() { - const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates } = useBlockEditor(); + const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates, setEditMode } = useBlockEditor(); const [inputs, setInputs] = useState(new Array(inputCount > 0 ? inputCount : 0).fill(0)); const [highlightedBlockId, setHighlightedBlockId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); @@ -265,6 +265,7 @@ export function BlockEditor() { }; const startOrResume = async (mode: StepMode, ignoreBreakpoints: boolean) => { + setEditMode(false); // kick user out of edit mode to ensure stable block values stepModeRef.current = mode; ignoreBreakpointsRef.current = ignoreBreakpoints; diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index acc89a5..55a0cc5 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -19,6 +19,7 @@ interface ToolbarProps { onEvaluationSpeedChange: (speed: number) => void; speedToText: (speed: number) => string; currentResult: number | null; + isEvaluating: boolean; } export function Toolbar({ @@ -38,9 +39,9 @@ export function Toolbar({ onEvaluationSpeedChange, speedToText, currentResult, + isEvaluating }: ToolbarProps) { const { editMode, setEditMode } = useBlockEditor(); - const [executionLocked, setExecutionLocked] = React.useState(false); return ( <> @@ -76,12 +77,12 @@ export function Toolbar({
@@ -89,27 +90,27 @@ export function Toolbar({ {/* Program execution controls */}
{/* run buttons lock edit; halt unlocks it - no async race condition */} - - - - -
diff --git a/recursive_blocks/src/BlockUtil.tsx b/recursive_blocks/src/BlockUtil.tsx index acc0ca5..71bd79f 100644 --- a/recursive_blocks/src/BlockUtil.tsx +++ b/recursive_blocks/src/BlockUtil.tsx @@ -1,4 +1,4 @@ -import { blockConfig, BlockType, BlockEvaluator, BlockSlot, ExecutionContext } from "./BlockConfig"; +import { blockConfig, BlockType, BlockSlot } from "./BlockConfig"; /* A custom data type that contains all the data for a block placed into the block tree. */ export interface BlockData { @@ -57,27 +57,15 @@ export async function evaluateBlock( return ev; } -// Step through block evaluation with optional execution context for visualization/callbacks -export async function stepBlock( - block: BlockData, - inputs: number[], - context?: ExecutionContext -): Promise { - const evaluateWithCallback: BlockEvaluator = async (b, i, _eval, ctx) => { - return await stepBlock(b, i, ctx); - }; - - if (context?.onStepCallback) { - await context.onStepCallback(block, null, inputs); - } - - const config = blockConfig[block.type]; - const ev = await config.evaluate(block, inputs, evaluateWithCallback, context); - - if (context?.onStepCallback) { - await context.onStepCallback(block, ev, inputs); +// Recursively collects all block IDs in a subtree (including the root). +export function collectDescendantIds(block: BlockData): string[] { + const ids = [block.id]; + for (const slot of block.children) { + if (slot.block) { + ids.push(...collectDescendantIds(slot.block)); + } } - return ev; + return ids; } // Takes in a defaultCount, and modifies it using the slot's input modifiers to get the actual input count the slot wants. diff --git a/recursive_blocks/src/Trace.ts b/recursive_blocks/src/Trace.ts new file mode 100644 index 0000000..159ab8c --- /dev/null +++ b/recursive_blocks/src/Trace.ts @@ -0,0 +1,37 @@ +import { BlockData, collectDescendantIds } from "./BlockUtil"; +import { blockConfig, BlockEvaluator, ExecutionContext } from "./BlockConfig"; + +export enum TraceEventType { + Enter, + Exit, + ClearSubtree +} + +export type TraceEvent = + | { type: TraceEventType.Enter; blockId: string; inputs: number[] } + | { type: TraceEventType.Exit; blockId: string; inputs: number[]; output: number } + | { type: TraceEventType.ClearSubtree; blockIds: string[] }; + +export async function generateTrace( + block: BlockData, + inputs: number[] +): Promise<{ result: number; events: TraceEvent[] }> { + const events: TraceEvent[] = []; + + const context: ExecutionContext = { + onClearSubtree: (blockToClear: BlockData) => { + events.push({ type: TraceEventType.ClearSubtree, blockIds: collectDescendantIds(blockToClear) }); + } + }; + + const tracingEvaluator: BlockEvaluator = async (b, i, _eval, _ctx) => { + events.push({ type: TraceEventType.Enter, blockId: b.id, inputs: [...i] }); + const config = blockConfig[b.type]; + const result = await config.evaluate(b, i, tracingEvaluator, context); + events.push({ type: TraceEventType.Exit, blockId: b.id, inputs: [...i], output: result }); + return result; + }; + + const result = await tracingEvaluator(block, inputs, tracingEvaluator, context); + return { result, events }; +} From ab6dbadc2e268688b812a2613b4dbc43f1b4ff59 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Wed, 25 Mar 2026 12:30:26 -0400 Subject: [PATCH 15/31] CSS Updates --- recursive_blocks/src/Block.css | 138 ++++++++++++++++++++++---- recursive_blocks/src/Block.tsx | 85 ++++++---------- recursive_blocks/src/BlockPalette.tsx | 17 +--- 3 files changed, 152 insertions(+), 88 deletions(-) diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 6723306..79bb3d1 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -96,12 +96,31 @@ .block-action-buttons { display: flex; - align-items: center; - gap: 0.1rem; + align-items: flex-start; + gap: 0.5rem; flex-shrink: 0; margin-left: auto; } +/* ===================== IO SPACING FIX START ===================== */ + +.block-io-container { + display: flex; + flex-direction: column; + align-items: flex-start; + + /* Force consistent spacing */ + gap: 6px; /* matches Successor visually */ +} + +/* Remove any accidental spacing inconsistencies */ +.block-io-in, +.block-io-out { + margin: 0; +} + +/* ====================== IO SPACING FIX END ====================== */ + .block-type { font-weight: 700; color: rgba(0,0,0,0.88); @@ -125,44 +144,54 @@ .block-type { color: rgba(255,255,255,0.92); } } -/* IO Display */ -.block-io-in { - display: flex; - align-items: center; - gap: 0.35rem; - width: max-content; - font-family: monospace; - font-size: 0.75rem; - padding-left: 0.2rem; - /*background-color: rgba(255,255,255,0.6);*/ -} +/* ===================== IO DISPLAY MINIMAL CLEAN START ===================== */ +.block-io-in, .block-io-out { display: flex; align-items: center; - gap: 0.35rem; + gap: 0.4rem; width: max-content; - font-family: monospace; + + font-family: inherit; font-size: 0.75rem; - margin-top: 0.5rem; - padding-left: 0.2rem; - /*background-color: rgba(255,255,255,0.6);*/ + + padding: 0.2rem 0.6rem; + + /* Clean neutral background */ + background: #ffffff; + + /* Uniform border on all sides */ + border: 1px solid rgba(0, 0, 0, 0.2); + + border-radius: 6px; + + /* No shadow, no effects */ + box-shadow: none; } +/* ====================== IO DISPLAY MINIMAL CLEAN END ====================== */ + +/* ===================== IO TEXT CLEAN START ===================== */ + .block-io-label { font-weight: 600; - color: rgba(0,0,0,0.95) !important; + color: #444; font-size: 0.75rem; - letter-spacing: 0.02em; + letter-spacing: 0.03em; } .block-io-value { font-weight: 700; - color: rgba(0,0,0,0.95) !important; + color: #000; min-width: 1.5rem; text-align: center; } +/* ====================== IO TEXT CLEAN END ====================== */ + +/* ====================== IO TEXT NEUTRAL UPGRADE END ====================== */ + .block-io-separator { color: rgba(0,0,0,0.6) !important; font-weight: 400; @@ -252,7 +281,7 @@ padding-left: 0.2rem; width: 0.9rem; flex-shrink: 0; - margin-top: 0.875rem; /* align roughly */ + margin-top: 0.875rem; } .composition-slot-content { @@ -312,6 +341,24 @@ .block-slot.empty { border-color: rgba(255,255,255,0.1); } } +@media (prefers-color-scheme: dark) { + .block-io-in, + .block-io-out { + background: #f4f4f4; + border: 1px solid rgba(0, 0, 0, 0.4); + } + + .block-io-label { + color: #333; + } + + .block-io-value { + color: #000; + } +} + + + /* Value Editor */ .value-editor { margin-top: 0.75rem; @@ -622,6 +669,53 @@ label { color: var(--block-text-color); } 50% { box-shadow: 0 0 20px 6px rgba(255,215,0,0.9); } } +/* Visual styles for Custom/Immutable blocks */ + +/* In the Editor */ +.block-custom-style { + filter: grayscale(0.9) brightness(0.9); + opacity: 0.85; +} + +.block-custom-style .block-type { + color: #555 !important; +} + +/* In the Sidebar Palette */ +.palette-block-custom { + background-color: #d1d1d1 !important; + border-left: 4px solid #888 !important; + color: #666 !important; + filter: grayscale(1); +} + +@media (prefers-color-scheme: dark) { + .block-custom-style { + filter: grayscale(0.7) brightness(0.8); + } + + .palette-block-custom { + background-color: #2d2d2d !important; + border-left: 4px solid #555 !important; + color: #999 !important; + } +} + +/* Highlight for updated IO values */ +.value-changed { + color: var(--contrast-col) !important; + font-weight: 800; + transition: color 0.2s ease; /* Returns to black over 0.2s */ + display: inline-block; + animation: valuePop 0.3s ease-out; +} + +@keyframes valuePop { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + /* Mobile */ @media (max-width: 768px) { #root { height: 100vh; overflow: hidden; } diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 430a83d..969538a 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -21,11 +21,8 @@ const getDepthColor = (depth: number) => { return colors[depth % colors.length]; }; -// Generate formal mathematical notation for each block type const getMathNotation = (block: BlockData): string => { const n = block.inputCount ?? 0; - - // Build argument list like (x1, x2, ..., xn) const args = (count: number, suffix?: string) => { if (count === 0) return suffix ? `(${suffix})` : "()"; const vars = Array.from({ length: count }, (_, i) => `x${i + 1}`).join(", "); @@ -33,10 +30,8 @@ const getMathNotation = (block: BlockData): string => { }; switch (block.type) { - case "Zero": - return `z${args(n)}`; - case "Successor": - return `s(x)`; + case "Zero": return `z${args(n)}`; + case "Successor": return `s(x)`; case "Projection": { const i = block.num_values?.find(v => v.name === "i")?.value ?? 1; return `id[${i},${n}]${args(n)}`; @@ -45,23 +40,12 @@ const getMathNotation = (block: BlockData): string => { const m = block.num_values?.find(v => v.name === "m")?.value ?? 1; return `C${n}[${m}]${args(n)}`; } - case "Primitive Recursion": - return `Pr[f,g]${args(n > 0 ? n - 1 : 0, "y")}`; - case "Minimization": - return `Mn[f]${args(n)}`; - default: - return ""; + case "Primitive Recursion": return `Pr[f,g]${args(n > 0 ? n - 1 : 0, "y")}`; + case "Minimization": return `Mn[f]${args(n)}`; + default: return ""; } }; -/* -A JSX element that represents a visual block element. -It is meant to be dragged into block slots. -Block is the BlockData of this block element. -onUpdate is a function that gets called when this block is modified -(meaning it is deleted, a value is modified, or an ancestor is modified) -and replaces the old block with the new block. -*/ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock, isRunning = false }: Props) { const [collapsed, setCollapsed] = React.useState(block?.collapsed ?? false); const [showInfo, setShowInfo] = React.useState(false); @@ -89,7 +73,12 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on const executionState = blockExecutionStates[block.id]; - // Format input/output for display + // Link highlight strictly to the active step + // This ensures that as soon as the 'Step' moves the highlight elsewhere, the color reverts. + const isCurrentlyActive = highlightedBlockId === block.id; + const showInputChange = isCurrentlyActive && executionState?.inputs !== undefined; + const showOutputChange = isCurrentlyActive && executionState?.output !== undefined; + const formatInput = (input: number[] | undefined) => { if (!input || input.length === 0) return '—'; return `(${input.join(', ')})`; @@ -103,14 +92,14 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
{ e.stopPropagation(); if (onSelectBlock && block) { @@ -142,37 +131,19 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
{block.errors.map((error, index) => ( -
- {error} -
+
{error}
))}
{blockConfig[block.type]?.description && ( - + )} {(block.children.length > 0 || (block.num_values && block.num_values.length > 0)) && ( - + )} {!block.immutable && ( - + )}
@@ -185,10 +156,14 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
In: - {formatInput(executionState?.inputs ?? block.latestInput)} + + {formatInput(executionState?.inputs ?? block.latestInput)} +
- {/* hide slots and ValueEditor when collapsed */} {!collapsed && (
{block.children.map((slot) => { @@ -226,7 +201,12 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
Out: - {formatOutput(executionState?.output ?? block.latestOutput)} + + {formatOutput(executionState?.output ?? block.latestOutput)} +
); @@ -236,17 +216,14 @@ export function getDefaultChildren(type: BlockType, depth: number = 0): Array ({ ...child, - block: child.block ? { - ...child.block, - depth: depth + 1 - } : null + block: child.block ? { ...child.block, depth: depth + 1 } : null })) : []; } export function getDefaultValues(type: BlockType): Array<{ name: string; value: number }> { const blockDef = blockConfig[type]; if (blockDef?.num_values) { - return blockDef.num_values.map(v => ({ ...v })); // return a copy + return blockDef.num_values.map(v => ({ ...v })); } return []; } \ No newline at end of file diff --git a/recursive_blocks/src/BlockPalette.tsx b/recursive_blocks/src/BlockPalette.tsx index a5e79a1..67b6d5e 100644 --- a/recursive_blocks/src/BlockPalette.tsx +++ b/recursive_blocks/src/BlockPalette.tsx @@ -7,10 +7,6 @@ import { BlockSave, serializeBlock } from "./BlockSave"; import { useBlockEditor } from "./BlockEditorContext"; import React from "react"; -// JSX element to represent a draggable block type in the block pallete -// Type is the block type. custom_block_index and onRemove are undefined if type is not Custom. -// If type is Custom, custom_block_index is an index in the custom blocks array corresponding to this block, -// and onRemove is a method to remove this custom block from the custom blocks array. function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType, custom_block_name?: string, onRemove?: (name: string) => void }) { const ref = useRef(null); const [showInfo, setShowInfo] = React.useState(false); @@ -32,9 +28,10 @@ function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType return (
- {/* use displayName from blockConfig if available */}
{custom_block_name !== undefined ? custom_block_name : (blockConfig[type]?.displayName ?? type.toUpperCase())}
{showInfo && blockConfig[type].description && ( @@ -54,11 +51,9 @@ function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType ); } -// JSX element that represents the block palette. -// Contains function to load custom blocks. export function BlockPalette() { - const { customBlockCount: _customBlockCount, setCustomBlockCount } = useBlockEditor(); - const {rootBlock, setRootBlock: _setRootBlock} = useBlockEditor(); + const { setCustomBlockCount } = useBlockEditor(); + const {rootBlock} = useBlockEditor(); function turnRootToCustom() { if (!rootBlock) { @@ -78,8 +73,6 @@ export function BlockPalette() { num_values: [] }; - // If the root block is a custom block, we want to save its first child as the definition of the new custom block. - // Otherwise, we save the root block itself as the definition of the new custom block. newBlock.children[0].child = serializeBlock(rootBlock.type !== "Custom" ? rootBlock : rootBlock.children[0].block!); customBlocks[name] = newBlock; From aa28eddbc1cea21d3b7bd9eb8cf0997738e52e06 Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 25 Mar 2026 12:30:31 -0400 Subject: [PATCH 16/31] new pr trace mode --- recursive_blocks/src/Block.css | 61 ++++++++++++++- recursive_blocks/src/Block.tsx | 83 ++++++++++++--------- recursive_blocks/src/BlockEditor.tsx | 38 +++++++++- recursive_blocks/src/BlockEditorContext.tsx | 17 ++++- recursive_blocks/src/PRTracePanel.tsx | 42 +++++++++++ recursive_blocks/src/Trace.ts | 65 ++++++++++++++++ 6 files changed, 265 insertions(+), 41 deletions(-) create mode 100644 recursive_blocks/src/PRTracePanel.tsx diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 6723306..8398379 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -178,7 +178,8 @@ /* Buttons */ .collapse-button, .info-button, -.remove-button { +.remove-button, +.pr-trace-toggle-button { font-size: 0.75rem; font-family: inherit; font-weight: 600; @@ -647,4 +648,62 @@ label { color: var(--block-text-color); } .block-io-label { font-size: 0.68rem; } .block-io-value { min-width: 1rem; } .sideP { padding: 1rem 0.5rem; } +} + +/* PR Trace Toggle Button */ +.pr-trace-toggle-button { + color: rgba(0,0,0,0.4); + font-weight: 700; + font-size: 0.8rem; +} +.pr-trace-toggle-button:hover { + background: rgba(255,255,255,0.08); + color: rgba(255,255,255,0.85); +} +.pr-trace-toggle-button.pr-trace-active { + color: var(--contrast-col); + background: rgba(223,109,38,0.12); +} + +/* PR Trace Panel */ +.pr-trace-panel { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + margin: 0.25rem 0.75rem; + border: 1px solid rgba(0,0,0,0.15); + border-radius: 6px; + background: rgba(255,255,255,0.15); +} + +.pr-trace-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.pr-trace-cell { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.pr-trace-label { + font-style: italic; + font-size: 0.8rem; + min-width: 3rem; + opacity: 0.7; +} + +.pr-trace-value { + font-family: 'Courier New', monospace; + font-size: 0.8rem; + padding: 0.15rem 0.5rem; + border: 1px solid rgba(0,0,0,0.2); + border-radius: 3px; + min-width: 2rem; + text-align: center; + background: rgba(255,255,255,0.25); } \ No newline at end of file diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 430a83d..e016fcb 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -2,10 +2,11 @@ import React from "react"; import { useDrag } from "react-dnd"; import { BlockData } from "./BlockUtil"; import './Block.css'; -import { blockConfig, BlockSlot, BlockType } from "./BlockConfig"; +import { blockConfig, BlockSlot, BlockType } from "./BlockConfig"; import { ValueEditor } from "./ValueEditor"; import { BlockSlotDisplay } from "./BlockSlot"; import { useBlockEditor } from "./BlockEditorContext"; +import { PRTracePanel } from "./PRTracePanel"; interface Props { block: BlockData | null; @@ -65,7 +66,7 @@ and replaces the old block with the new block. export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock, isRunning = false }: Props) { const [collapsed, setCollapsed] = React.useState(block?.collapsed ?? false); const [showInfo, setShowInfo] = React.useState(false); - const { blockExecutionStates } = useBlockEditor(); + const { blockExecutionStates, prTraceMode, setPRTraceMode, prTraceFrames } = useBlockEditor(); if (!block) { return Drop block here; @@ -158,6 +159,15 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on i )} + {block.type === "Primitive Recursion" && ( + + )} {(block.children.length > 0 || (block.num_values && block.num_values.length > 0)) && (
- )} + block.type === "Primitive Recursion" && prTraceMode[block.id] + ? + : ( + <> +
+ {block.children.map((slot) => { + const slotDisplay = ( + + ); + + if (block.type === "Composition") { + const label = slot.name.startsWith("g") + ? g{slot.name.slice(1)} + : f; - {!collapsed && } + return ( +
+
{label}:
+
{slotDisplay}
+
+ ); + } + + return
{slotDisplay}
; + })} +
+ + + ) + )}
Out: diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 448a76c..1ac70a0 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -7,7 +7,7 @@ import { useBlockEditor } from "./BlockEditorContext"; import { BlockSlotDisplay } from "./BlockSlot"; import { BlockPalette } from "./BlockPalette"; import { removeBlockById } from "./BlockUtil"; -import { generateTrace, TraceEvent, TraceEventType } from "./Trace"; +import { generateTrace, TraceEvent, TraceEventType, buildPRFrames, PRTraceFrame } from "./Trace"; export interface EditorSaveState { fileType: string; @@ -32,7 +32,7 @@ enum PlaybackMode { // JSX element that represents the editor, containing a root block and the header. export function BlockEditor() { - const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates, setEditMode } = useBlockEditor(); + const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates, setEditMode, prTraceMode, setPRTraceFrames } = useBlockEditor(); const [inputs, setInputs] = useState(new Array(inputCount > 0 ? inputCount : 0).fill(0)); const [highlightedBlockId, setHighlightedBlockId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); @@ -51,6 +51,10 @@ export function BlockEditor() { const finalResultRef = useRef(null); const playbackDepthsRef = useRef>(new Map()); + // PR trace panel state + const prFrameListRef = useRef>({}); + const prCursorRef = useRef>({}); + useEffect(() => { const newBreakpoints = new Set(); const traverse = (b: BlockData) => { @@ -262,13 +266,26 @@ export function BlockEditor() { cursorRef.current = 0; finalResultRef.current = null; playbackDepthsRef.current.clear(); + prFrameListRef.current = {}; + prCursorRef.current = {}; setBlockExecutionStates({}); + setPRTraceFrames({}); setCurrentResult(null); setHighlightedBlockId(null); setIsEvaluating(false); setPaused(false); }; + const advancePRPanel = (blockId: string) => { + if (!prTraceMode[blockId] || !prFrameListRef.current[blockId]) return; + const cursor = prCursorRef.current[blockId] ?? 0; + const frames = prFrameListRef.current[blockId]; + if (cursor < frames.length) { + setPRTraceFrames(prev => ({ ...prev, [blockId]: frames[cursor] })); + prCursorRef.current[blockId] = cursor + 1; + } + }; + const advance = async (mode: PlaybackMode) => { const trace = traceRef.current; if (!trace) return; @@ -293,6 +310,9 @@ export function BlockEditor() { const d = depths.get(event.blockId) || 0; depths.set(event.blockId, d + 1); + // Advance PR trace panel if this block is in trace mode + advancePRPanel(event.blockId); + let shouldPause = false; if (mode === PlaybackMode.Trace) shouldPause = true; if (mode === PlaybackMode.Run && breakpointsRef.current.has(event.blockId) && d === 0) { @@ -315,6 +335,9 @@ export function BlockEditor() { const d = depths.get(event.blockId) || 0; depths.set(event.blockId, Math.max(0, d - 1)); + // Advance PR trace panel if this block is in trace mode + advancePRPanel(event.blockId); + let shouldPause = false; if (mode === PlaybackMode.Trace) shouldPause = true; if (mode === PlaybackMode.Step) shouldPause = true; @@ -379,6 +402,17 @@ export function BlockEditor() { traceRef.current = events; cursorRef.current = 0; finalResultRef.current = result; + + // Build PR trace frames for all PR blocks in trace mode + const prBlockIds = Object.keys(prTraceMode).filter(id => prTraceMode[id]); + prFrameListRef.current = {}; + prCursorRef.current = {}; + for (const id of prBlockIds) { + prFrameListRef.current[id] = buildPRFrames(events, id); + prCursorRef.current[id] = 0; + } + setPRTraceFrames({}); + await advance(mode); } catch (error) { if (error instanceof Error) { diff --git a/recursive_blocks/src/BlockEditorContext.tsx b/recursive_blocks/src/BlockEditorContext.tsx index 3bea7c5..4e023e4 100644 --- a/recursive_blocks/src/BlockEditorContext.tsx +++ b/recursive_blocks/src/BlockEditorContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState } from "react"; import { BlockData } from "./BlockUtil"; import { DEFAULT_INPUT_COUNT } from "./BlockEditor"; +import { PRTraceFrame } from "./Trace"; /* Defines a React Context, which makes the variables inside accessable by any React element within the context. @@ -19,6 +20,10 @@ const BlockEditorContext = createContext<{ setBlockExecutionStates: React.Dispatch>>; editMode: boolean; setEditMode: React.Dispatch>; + prTraceMode: Record; + setPRTraceMode: React.Dispatch>>; + prTraceFrames: Record; + setPRTraceFrames: React.Dispatch>>; } | null>(null); export function BlockEditorProvider({ children }: { children: React.ReactNode }) { @@ -27,14 +32,18 @@ export function BlockEditorProvider({ children }: { children: React.ReactNode }) const [customBlockCount, setCustomBlockCount] = useState(0); const [blockExecutionStates, setBlockExecutionStates] = useState>({}); const [editMode, setEditMode] = useState(false); - + const [prTraceMode, setPRTraceMode] = useState>({}); + const [prTraceFrames, setPRTraceFrames] = useState>({}); + return ( - {children} diff --git a/recursive_blocks/src/PRTracePanel.tsx b/recursive_blocks/src/PRTracePanel.tsx new file mode 100644 index 0000000..ddbd8e6 --- /dev/null +++ b/recursive_blocks/src/PRTracePanel.tsx @@ -0,0 +1,42 @@ +import { PRTraceFrame } from "./Trace"; +import './Block.css'; + +interface Props { + frame?: PRTraceFrame | null; +} + +export function PRTracePanel({ frame }: Props) { + const fmtVals = (v: number[] | null | undefined) => { + if (!v || v.length === 0) return '\u2014'; + return v.join(', '); + }; + + const fmtResult = (v: number | null | undefined) => { + return v !== null && v !== undefined ? String(v) : '\u2014'; + }; + + return ( +
+
+
+ x,y' + {fmtVals(frame?.xy_prime)} +
+
+ h(x,y') + {fmtResult(frame?.hxy_prime)} +
+
+
+
+ x,y + {fmtVals(frame?.xy)} +
+
+ h(x,y) + {fmtResult(frame?.hxy)} +
+
+
+ ); +} diff --git a/recursive_blocks/src/Trace.ts b/recursive_blocks/src/Trace.ts index 159ab8c..0a83f9b 100644 --- a/recursive_blocks/src/Trace.ts +++ b/recursive_blocks/src/Trace.ts @@ -35,3 +35,68 @@ export async function generateTrace( const result = await tracingEvaluator(block, inputs, tracingEvaluator, context); return { result, events }; } + +// Primitive Recursion Trace Panel +export type PRTraceFrame = { + xy_prime: number[]; // x,y' inputs at the current recursion level + xy: number[] | null; // x,y inputs at the connected level (null at base case) + hxy: number | null; // h(x,y) result from the connected level + hxy_prime: number | null; // h(x,y') result at the current level +}; + +/** + * Extract PR-specific animation frames from a flat trace. + * Each Enter/Exit event for the given PR block maps 1:1 to a frame. + * Descent frames show inputs flowing down; ascent frames show outputs building up. + */ +export function buildPRFrames(events: TraceEvent[], prBlockId: string): PRTraceFrame[] { + const enters: Array<{ inputs: number[] }> = []; + const exits: Array<{ inputs: number[]; output: number }> = []; + + for (const e of events) { + if (e.type === TraceEventType.Enter && e.blockId === prBlockId) { + enters.push({ inputs: e.inputs }); + } else if (e.type === TraceEventType.Exit && e.blockId === prBlockId) { + exits.push({ inputs: e.inputs, output: e.output }); + } + } + + const frames: PRTraceFrame[] = []; + + // Descent frames (one per Enter event) + for (const e of enters) { + const y = e.inputs[e.inputs.length - 1]; + if (y > 0) { + frames.push({ + xy_prime: e.inputs, + xy: [...e.inputs.slice(0, -1), y - 1], + hxy: null, + hxy_prime: null, + }); + } else { + // Base case enter: inputs visible, results pending + frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: null }); + } + } + + // Exit frames (base case first, then ascent) + for (let i = 0; i < exits.length; i++) { + const e = exits[i]; + const y = e.inputs[e.inputs.length - 1]; + if (y === 0) { + // Base case exit: reveal h(x,y') + frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: e.output }); + } else { + // Ascent: show current level and the level below it + const prevOutput = i > 0 ? exits[i - 1].output : null; + frames.push({ + xy_prime: e.inputs, + xy: [...e.inputs.slice(0, -1), y - 1], + hxy: prevOutput, + hxy_prime: e.output, + }); + } + } + + return frames; +} From b0d26f730023a27faf1a8dc6d7472663a88119c6 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Wed, 25 Mar 2026 12:30:26 -0400 Subject: [PATCH 17/31] CSS Updates --- recursive_blocks/src/Block.css | 138 ++++++++++++++++++++++---- recursive_blocks/src/Block.tsx | 85 ++++++---------- recursive_blocks/src/BlockPalette.tsx | 17 +--- 3 files changed, 152 insertions(+), 88 deletions(-) diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 8398379..8e58b30 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -96,12 +96,31 @@ .block-action-buttons { display: flex; - align-items: center; - gap: 0.1rem; + align-items: flex-start; + gap: 0.5rem; flex-shrink: 0; margin-left: auto; } +/* ===================== IO SPACING FIX START ===================== */ + +.block-io-container { + display: flex; + flex-direction: column; + align-items: flex-start; + + /* Force consistent spacing */ + gap: 6px; /* matches Successor visually */ +} + +/* Remove any accidental spacing inconsistencies */ +.block-io-in, +.block-io-out { + margin: 0; +} + +/* ====================== IO SPACING FIX END ====================== */ + .block-type { font-weight: 700; color: rgba(0,0,0,0.88); @@ -125,44 +144,54 @@ .block-type { color: rgba(255,255,255,0.92); } } -/* IO Display */ -.block-io-in { - display: flex; - align-items: center; - gap: 0.35rem; - width: max-content; - font-family: monospace; - font-size: 0.75rem; - padding-left: 0.2rem; - /*background-color: rgba(255,255,255,0.6);*/ -} +/* ===================== IO DISPLAY MINIMAL CLEAN START ===================== */ +.block-io-in, .block-io-out { display: flex; align-items: center; - gap: 0.35rem; + gap: 0.4rem; width: max-content; - font-family: monospace; + + font-family: inherit; font-size: 0.75rem; - margin-top: 0.5rem; - padding-left: 0.2rem; - /*background-color: rgba(255,255,255,0.6);*/ + + padding: 0.2rem 0.6rem; + + /* Clean neutral background */ + background: #ffffff; + + /* Uniform border on all sides */ + border: 1px solid rgba(0, 0, 0, 0.2); + + border-radius: 6px; + + /* No shadow, no effects */ + box-shadow: none; } +/* ====================== IO DISPLAY MINIMAL CLEAN END ====================== */ + +/* ===================== IO TEXT CLEAN START ===================== */ + .block-io-label { font-weight: 600; - color: rgba(0,0,0,0.95) !important; + color: #444; font-size: 0.75rem; - letter-spacing: 0.02em; + letter-spacing: 0.03em; } .block-io-value { font-weight: 700; - color: rgba(0,0,0,0.95) !important; + color: #000; min-width: 1.5rem; text-align: center; } +/* ====================== IO TEXT CLEAN END ====================== */ + +/* ====================== IO TEXT NEUTRAL UPGRADE END ====================== */ + .block-io-separator { color: rgba(0,0,0,0.6) !important; font-weight: 400; @@ -253,7 +282,7 @@ padding-left: 0.2rem; width: 0.9rem; flex-shrink: 0; - margin-top: 0.875rem; /* align roughly */ + margin-top: 0.875rem; } .composition-slot-content { @@ -313,6 +342,24 @@ .block-slot.empty { border-color: rgba(255,255,255,0.1); } } +@media (prefers-color-scheme: dark) { + .block-io-in, + .block-io-out { + background: #f4f4f4; + border: 1px solid rgba(0, 0, 0, 0.4); + } + + .block-io-label { + color: #333; + } + + .block-io-value { + color: #000; + } +} + + + /* Value Editor */ .value-editor { margin-top: 0.75rem; @@ -623,6 +670,53 @@ label { color: var(--block-text-color); } 50% { box-shadow: 0 0 20px 6px rgba(255,215,0,0.9); } } +/* Visual styles for Custom/Immutable blocks */ + +/* In the Editor */ +.block-custom-style { + filter: grayscale(0.9) brightness(0.9); + opacity: 0.85; +} + +.block-custom-style .block-type { + color: #555 !important; +} + +/* In the Sidebar Palette */ +.palette-block-custom { + background-color: #d1d1d1 !important; + border-left: 4px solid #888 !important; + color: #666 !important; + filter: grayscale(1); +} + +@media (prefers-color-scheme: dark) { + .block-custom-style { + filter: grayscale(0.7) brightness(0.8); + } + + .palette-block-custom { + background-color: #2d2d2d !important; + border-left: 4px solid #555 !important; + color: #999 !important; + } +} + +/* Highlight for updated IO values */ +.value-changed { + color: var(--contrast-col) !important; + font-weight: 800; + transition: color 0.2s ease; /* Returns to black over 0.2s */ + display: inline-block; + animation: valuePop 0.3s ease-out; +} + +@keyframes valuePop { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + /* Mobile */ @media (max-width: 768px) { #root { height: 100vh; overflow: hidden; } diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index e016fcb..40340d3 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -22,11 +22,8 @@ const getDepthColor = (depth: number) => { return colors[depth % colors.length]; }; -// Generate formal mathematical notation for each block type const getMathNotation = (block: BlockData): string => { const n = block.inputCount ?? 0; - - // Build argument list like (x1, x2, ..., xn) const args = (count: number, suffix?: string) => { if (count === 0) return suffix ? `(${suffix})` : "()"; const vars = Array.from({ length: count }, (_, i) => `x${i + 1}`).join(", "); @@ -34,10 +31,8 @@ const getMathNotation = (block: BlockData): string => { }; switch (block.type) { - case "Zero": - return `z${args(n)}`; - case "Successor": - return `s(x)`; + case "Zero": return `z${args(n)}`; + case "Successor": return `s(x)`; case "Projection": { const i = block.num_values?.find(v => v.name === "i")?.value ?? 1; return `id[${i},${n}]${args(n)}`; @@ -46,23 +41,12 @@ const getMathNotation = (block: BlockData): string => { const m = block.num_values?.find(v => v.name === "m")?.value ?? 1; return `C${n}[${m}]${args(n)}`; } - case "Primitive Recursion": - return `Pr[f,g]${args(n > 0 ? n - 1 : 0, "y")}`; - case "Minimization": - return `Mn[f]${args(n)}`; - default: - return ""; + case "Primitive Recursion": return `Pr[f,g]${args(n > 0 ? n - 1 : 0, "y")}`; + case "Minimization": return `Mn[f]${args(n)}`; + default: return ""; } }; -/* -A JSX element that represents a visual block element. -It is meant to be dragged into block slots. -Block is the BlockData of this block element. -onUpdate is a function that gets called when this block is modified -(meaning it is deleted, a value is modified, or an ancestor is modified) -and replaces the old block with the new block. -*/ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock, isRunning = false }: Props) { const [collapsed, setCollapsed] = React.useState(block?.collapsed ?? false); const [showInfo, setShowInfo] = React.useState(false); @@ -90,7 +74,12 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on const executionState = blockExecutionStates[block.id]; - // Format input/output for display + // Link highlight strictly to the active step + // This ensures that as soon as the 'Step' moves the highlight elsewhere, the color reverts. + const isCurrentlyActive = highlightedBlockId === block.id; + const showInputChange = isCurrentlyActive && executionState?.inputs !== undefined; + const showOutputChange = isCurrentlyActive && executionState?.output !== undefined; + const formatInput = (input: number[] | undefined) => { if (!input || input.length === 0) return '—'; return `(${input.join(', ')})`; @@ -104,14 +93,14 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
{ e.stopPropagation(); if (onSelectBlock && block) { @@ -143,21 +132,13 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
{block.errors.map((error, index) => ( -
- {error} -
+
{error}
))}
{blockConfig[block.type]?.description && ( - + )} {block.type === "Primitive Recursion" && ( + )} {!block.immutable && ( - + )}
@@ -195,10 +166,14 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
In: - {formatInput(executionState?.inputs ?? block.latestInput)} + + {formatInput(executionState?.inputs ?? block.latestInput)} +
- {/* hide slots and ValueEditor when collapsed */} {!collapsed && ( block.type === "Primitive Recursion" && prTraceMode[block.id] ? @@ -241,7 +216,12 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
Out: - {formatOutput(executionState?.output ?? block.latestOutput)} + + {formatOutput(executionState?.output ?? block.latestOutput)} +
); @@ -251,17 +231,14 @@ export function getDefaultChildren(type: BlockType, depth: number = 0): Array ({ ...child, - block: child.block ? { - ...child.block, - depth: depth + 1 - } : null + block: child.block ? { ...child.block, depth: depth + 1 } : null })) : []; } export function getDefaultValues(type: BlockType): Array<{ name: string; value: number }> { const blockDef = blockConfig[type]; if (blockDef?.num_values) { - return blockDef.num_values.map(v => ({ ...v })); // return a copy + return blockDef.num_values.map(v => ({ ...v })); } return []; } \ No newline at end of file diff --git a/recursive_blocks/src/BlockPalette.tsx b/recursive_blocks/src/BlockPalette.tsx index a5e79a1..67b6d5e 100644 --- a/recursive_blocks/src/BlockPalette.tsx +++ b/recursive_blocks/src/BlockPalette.tsx @@ -7,10 +7,6 @@ import { BlockSave, serializeBlock } from "./BlockSave"; import { useBlockEditor } from "./BlockEditorContext"; import React from "react"; -// JSX element to represent a draggable block type in the block pallete -// Type is the block type. custom_block_index and onRemove are undefined if type is not Custom. -// If type is Custom, custom_block_index is an index in the custom blocks array corresponding to this block, -// and onRemove is a method to remove this custom block from the custom blocks array. function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType, custom_block_name?: string, onRemove?: (name: string) => void }) { const ref = useRef(null); const [showInfo, setShowInfo] = React.useState(false); @@ -32,9 +28,10 @@ function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType return (
- {/* use displayName from blockConfig if available */}
{custom_block_name !== undefined ? custom_block_name : (blockConfig[type]?.displayName ?? type.toUpperCase())}
{showInfo && blockConfig[type].description && ( @@ -54,11 +51,9 @@ function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType ); } -// JSX element that represents the block palette. -// Contains function to load custom blocks. export function BlockPalette() { - const { customBlockCount: _customBlockCount, setCustomBlockCount } = useBlockEditor(); - const {rootBlock, setRootBlock: _setRootBlock} = useBlockEditor(); + const { setCustomBlockCount } = useBlockEditor(); + const {rootBlock} = useBlockEditor(); function turnRootToCustom() { if (!rootBlock) { @@ -78,8 +73,6 @@ export function BlockPalette() { num_values: [] }; - // If the root block is a custom block, we want to save its first child as the definition of the new custom block. - // Otherwise, we save the root block itself as the definition of the new custom block. newBlock.children[0].child = serializeBlock(rootBlock.type !== "Custom" ? rootBlock : rootBlock.children[0].block!); customBlocks[name] = newBlock; From 031e780d9e586b585976382d1614339a4679235f Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 31 Mar 2026 22:21:56 -0400 Subject: [PATCH 18/31] More CSS changes --- recursive_blocks/src/Block.css | 11 ++-- recursive_blocks/src/Block.tsx | 52 ++++++++++++++++--- recursive_blocks/src/BlockEditor.tsx | 43 +++++++++++++--- recursive_blocks/src/BlockPalette.tsx | 18 ++++++- recursive_blocks/src/Toolbar.tsx | 5 +- recursive_blocks/src/ValueEditor.tsx | 74 ++++++++++++++++++--------- 6 files changed, 159 insertions(+), 44 deletions(-) diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 8e58b30..3bad0f2 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -674,14 +674,17 @@ label { color: var(--block-text-color); } /* In the Editor */ .block-custom-style { - filter: grayscale(0.9) brightness(0.9); - opacity: 0.85; + opacity: 0.92; } .block-custom-style .block-type { color: #555 !important; } +.block-custom-style .block-math-notation { + color: rgba(60, 60, 60, 0.78); +} + /* In the Sidebar Palette */ .palette-block-custom { background-color: #d1d1d1 !important; @@ -692,7 +695,7 @@ label { color: var(--block-text-color); } @media (prefers-color-scheme: dark) { .block-custom-style { - filter: grayscale(0.7) brightness(0.8); + opacity: 0.9; } .palette-block-custom { @@ -800,4 +803,4 @@ label { color: var(--block-text-color); } min-width: 2rem; text-align: center; background: rgba(255,255,255,0.25); -} \ No newline at end of file +} diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 40340d3..1c8a0b8 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -17,9 +17,43 @@ interface Props { isRunning?: boolean; } -const getDepthColor = (depth: number) => { - const colors = ['#ff9999', '#99ff99', '#9999ff', '#ffff99', '#ff99ff', '#99ffff']; - return colors[depth % colors.length]; +const DEPTH_COLORS = ['#ff9999', '#99ff99', '#9999ff', '#ffff99', '#ff99ff', '#99ffff']; +const CUSTOM_BLOCK_TINT = '#9a9a9a'; + +const getDepthColor = (depth: number) => DEPTH_COLORS[depth % DEPTH_COLORS.length]; + +const hexToRgb = (hex: string) => { + const normalized = hex.replace('#', ''); + const value = normalized.length === 3 + ? normalized.split('').map((char) => char + char).join('') + : normalized; + + const parsed = Number.parseInt(value, 16); + + return { + r: (parsed >> 16) & 255, + g: (parsed >> 8) & 255, + b: parsed & 255, + }; +}; + +const blendHexColors = (base: string, tint: string, tintWeight: number) => { + const baseRgb = hexToRgb(base); + const tintRgb = hexToRgb(tint); + const baseWeight = 1 - tintWeight; + const blendChannel = (baseChannel: number, tintChannel: number) => + Math.round(baseChannel * baseWeight + tintChannel * tintWeight); + + return `rgb(${blendChannel(baseRgb.r, tintRgb.r)}, ${blendChannel(baseRgb.g, tintRgb.g)}, ${blendChannel(baseRgb.b, tintRgb.b)})`; +}; + +const getCustomBlockColors = (depth: number) => { + const baseColor = getDepthColor(depth); + + return { + background: blendHexColors(baseColor, CUSTOM_BLOCK_TINT, 0.72), + border: blendHexColors(baseColor, CUSTOM_BLOCK_TINT, 0.56), + }; }; const getMathNotation = (block: BlockData): string => { @@ -73,6 +107,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on drag(dragRef); const executionState = blockExecutionStates[block.id]; + const isMutedCustomBlock = block.type === "Custom" || block.immutable; // Link highlight strictly to the active step // This ensures that as soon as the 'Step' moves the highlight elsewhere, the color reverts. @@ -89,17 +124,20 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on return output !== undefined ? output.toString() : '—'; }; + const depthColor = getDepthColor(block.depth || 0); + const customColors = isMutedCustomBlock ? getCustomBlockColors(block.depth || 0) : null; + return (
{ e.stopPropagation(); @@ -241,4 +279,4 @@ export function getDefaultValues(type: BlockType): Array<{ name: string; value: return blockDef.num_values.map(v => ({ ...v })); } return []; -} \ No newline at end of file +} diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 1ac70a0..fa5565d 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -20,9 +20,16 @@ export interface EditorSaveState { export const CURRENT_FILETYPE_VERSION = "BRAM_EDITOR_STATE_V2"; export const DEFAULT_INPUT_COUNT = 2; +export const MAX_INPUT_COUNT = 12; +export const MAX_CUSTOM_BLOCKS = 40; export const customBlocks: Record = {}; +const clampInputCount = (count: number) => { + if (!Number.isFinite(count)) return 0; + return Math.min(MAX_INPUT_COUNT, Math.max(0, count)); +}; + enum PlaybackMode { Run, ForceRun, @@ -99,7 +106,10 @@ export function BlockEditor() { }, [inputCount, rootBlock]); const handleInputCountChange = (count: number) => { - const clamped = Math.max(0, count); + if (Number.isFinite(count) && count > MAX_INPUT_COUNT) { + alert(`Input count is capped at ${MAX_INPUT_COUNT}.`); + } + const clamped = clampInputCount(count); setInputCount(clamped); }; @@ -233,14 +243,35 @@ export function BlockEditor() { throw new Error("Invalid or incompatible .bramflower file."); } + const clampedInputCount = clampInputCount(loadedState.inputCount); + const normalizedInputs = Array.from( + { length: clampedInputCount }, + (_, i) => loadedState.inputs[i] ?? 0 + ); + setRootBlock(loadedState.rootBlock ? deserializeBlock(loadedState.rootBlock) : null); - setInputs(loadedState.inputs); - setInputCount(loadedState.inputCount); + setInputs(normalizedInputs); + setInputCount(clampedInputCount); - setCustomBlockCount(Object.keys(customBlocks).length); - for (const [name, customBlock] of Object.entries(loadedState.customBlocks)) { + const nextCustomBlocks = Object.entries(loadedState.customBlocks ?? {}); + const limitedCustomBlocks = nextCustomBlocks.slice(0, MAX_CUSTOM_BLOCKS); + + Object.keys(customBlocks).forEach((key) => { + delete customBlocks[key]; + }); + + for (const [name, customBlock] of limitedCustomBlocks) { customBlocks[customBlock.name ?? name] = customBlock; } + setCustomBlockCount(Object.keys(customBlocks).length); + + if (loadedState.inputCount !== clampedInputCount) { + alert(`Loaded file used ${loadedState.inputCount} inputs. The editor cap is ${MAX_INPUT_COUNT}, so it was reduced.`); + } + + if (nextCustomBlocks.length > MAX_CUSTOM_BLOCKS) { + alert(`Loaded file had ${nextCustomBlocks.length} custom blocks. Only the first ${MAX_CUSTOM_BLOCKS} were loaded.`); + } console.log("State loaded successfully."); } catch (error) { @@ -482,4 +513,4 @@ export function BlockEditor() {
); -} \ No newline at end of file +} diff --git a/recursive_blocks/src/BlockPalette.tsx b/recursive_blocks/src/BlockPalette.tsx index 67b6d5e..7e60952 100644 --- a/recursive_blocks/src/BlockPalette.tsx +++ b/recursive_blocks/src/BlockPalette.tsx @@ -1,12 +1,16 @@ import { useRef, useEffect } from "react"; import { useDrag } from "react-dnd"; import { blockConfig, BlockType } from "./BlockConfig"; -import { CURRENT_FILETYPE_VERSION, customBlocks, EditorSaveState } from "./BlockEditor"; +import { CURRENT_FILETYPE_VERSION, customBlocks, EditorSaveState, MAX_CUSTOM_BLOCKS } from "./BlockEditor"; import { getDefaultChildren } from "./Block"; import { BlockSave, serializeBlock } from "./BlockSave"; import { useBlockEditor } from "./BlockEditorContext"; import React from "react"; +function canAddCustomBlock() { + return Object.keys(customBlocks).length < MAX_CUSTOM_BLOCKS; +} + function DraggableBlock({ type, custom_block_name, onRemove }: { type: BlockType, custom_block_name?: string, onRemove?: (name: string) => void }) { const ref = useRef(null); const [showInfo, setShowInfo] = React.useState(false); @@ -61,6 +65,11 @@ export function BlockPalette() { return; } + if (!canAddCustomBlock()) { + alert(`Custom block limit reached. You can load up to ${MAX_CUSTOM_BLOCKS} custom blocks at once.`); + return; + } + const name = prompt("Enter a name for the new custom block:"); if (!name) { alert("Block creation cancelled."); @@ -99,6 +108,11 @@ export function BlockPalette() { return; } + if (!canAddCustomBlock()) { + alert(`Custom block limit reached. You can load up to ${MAX_CUSTOM_BLOCKS} custom blocks at once.`); + return; + } + let newBlock: BlockSave; if (parsed.rootBlock.type == "Custom") { newBlock = parsed.rootBlock; @@ -162,4 +176,4 @@ export function BlockPalette() {
); -} \ No newline at end of file +} diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index 55a0cc5..1e1b11c 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -1,6 +1,7 @@ import React, { } from "react"; import './Toolbar.css'; import { useBlockEditor } from "./BlockEditorContext"; +import { MAX_INPUT_COUNT } from "./BlockEditor"; interface ToolbarProps { onSave: () => void; @@ -124,7 +125,7 @@ export function Toolbar({ onInputCountChange(parseInt(e.target.value))} @@ -184,4 +185,4 @@ export function Toolbar({
); -} \ No newline at end of file +} diff --git a/recursive_blocks/src/ValueEditor.tsx b/recursive_blocks/src/ValueEditor.tsx index a459a9f..3f5c374 100644 --- a/recursive_blocks/src/ValueEditor.tsx +++ b/recursive_blocks/src/ValueEditor.tsx @@ -16,6 +16,30 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP const [values, setValues] = useState(block.num_values ?? []); const { editMode } = useBlockEditor(); + const formatStaticValue = (valueName: string, value: number) => { + if (block.type === "Projection" && valueName === "i") { + const inputCount = Math.max(block.inputCount, value, 1); + const args = Array.from({ length: inputCount }, (_, i) => `x${i + 1}`).join(", "); + + return { + label: `(${args}) →`, + value: `x${value}`, + }; + } + + if (block.type === "Composition" && valueName === "m") { + return { + label: "arity →", + value: `${value} inner block${value === 1 ? "" : "s"}`, + }; + } + + return { + label: `${valueName} →`, + value: value.toString(), + }; + }; + useEffect(() => { if (JSON.stringify(values) !== JSON.stringify(block.num_values)) { onUpdate({ ...block, num_values: values, depth: block.depth || 0 }); @@ -34,28 +58,32 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP return (
- {values.map((val, index) => ( -
- {isRunning || block.immutable || !editMode ? ( - - {val.name} → - {val.value} - - ) : ( - <> - - v.name === val.name)?.min || 0} - step="1" - value={val.value} - onChange={(e) => handleValueChange(index, e.target.value)} - className="value-input" - /> - - )} -
- ))} + {values.map((val, index) => { + const staticValue = formatStaticValue(val.name, val.value); + + return ( +
+ {isRunning || !editMode ? ( + + {staticValue.label} + {staticValue.value} + + ) : ( + <> + + v.name === val.name)?.min || 0} + step="1" + value={val.value} + onChange={(e) => handleValueChange(index, e.target.value)} + className="value-input" + /> + + )} +
+ ); + })}
); -} \ No newline at end of file +} From 7393c3d4d13ea7ebe7574a65310c8fbda08c029f Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 25 Mar 2026 16:36:43 -0400 Subject: [PATCH 19/31] fix buildprframes for nested pr blocks --- recursive_blocks/src/Trace.ts | 79 +++++++++++++++++------------------ 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/recursive_blocks/src/Trace.ts b/recursive_blocks/src/Trace.ts index 0a83f9b..5eacccd 100644 --- a/recursive_blocks/src/Trace.ts +++ b/recursive_blocks/src/Trace.ts @@ -50,51 +50,50 @@ export type PRTraceFrame = { * Descent frames show inputs flowing down; ascent frames show outputs building up. */ export function buildPRFrames(events: TraceEvent[], prBlockId: string): PRTraceFrame[] { - const enters: Array<{ inputs: number[] }> = []; - const exits: Array<{ inputs: number[]; output: number }> = []; + const frames: PRTraceFrame[] = []; + let depth = 0; + let prevExitOutput: number | null = null; for (const e of events) { - if (e.type === TraceEventType.Enter && e.blockId === prBlockId) { - enters.push({ inputs: e.inputs }); - } else if (e.type === TraceEventType.Exit && e.blockId === prBlockId) { - exits.push({ inputs: e.inputs, output: e.output }); - } - } + if (e.type === TraceEventType.ClearSubtree) continue; + if (e.blockId !== prBlockId) continue; - const frames: PRTraceFrame[] = []; + if (e.type === TraceEventType.Enter) { + if (depth === 0) { + // New invocation — reset ascent state + prevExitOutput = null; + } + depth++; - // Descent frames (one per Enter event) - for (const e of enters) { - const y = e.inputs[e.inputs.length - 1]; - if (y > 0) { - frames.push({ - xy_prime: e.inputs, - xy: [...e.inputs.slice(0, -1), y - 1], - hxy: null, - hxy_prime: null, - }); - } else { - // Base case enter: inputs visible, results pending - frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: null }); - } - } + const y = e.inputs[e.inputs.length - 1]; + if (y > 0) { + frames.push({ + xy_prime: e.inputs, + xy: [...e.inputs.slice(0, -1), y - 1], + hxy: null, + hxy_prime: null, + }); + } else { + // Base case enter: inputs visible, results pending + frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: null }); + } + } else if (e.type === TraceEventType.Exit) { + depth--; - // Exit frames (base case first, then ascent) - for (let i = 0; i < exits.length; i++) { - const e = exits[i]; - const y = e.inputs[e.inputs.length - 1]; - if (y === 0) { - // Base case exit: reveal h(x,y') - frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: e.output }); - } else { - // Ascent: show current level and the level below it - const prevOutput = i > 0 ? exits[i - 1].output : null; - frames.push({ - xy_prime: e.inputs, - xy: [...e.inputs.slice(0, -1), y - 1], - hxy: prevOutput, - hxy_prime: e.output, - }); + const y = e.inputs[e.inputs.length - 1]; + if (y === 0) { + // Base case exit: reveal h(x,y') + frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: e.output }); + } else { + // Ascent: show current level with result from the level below + frames.push({ + xy_prime: e.inputs, + xy: [...e.inputs.slice(0, -1), y - 1], + hxy: prevExitOutput, + hxy_prime: e.output, + }); + } + prevExitOutput = e.output; } } From 0529669c5c5b364165f0d4d27f1fe21d6e9101c3 Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 1 Apr 2026 03:06:55 -0400 Subject: [PATCH 20/31] fix trace panel issues --- recursive_blocks/src/BlockEditor.tsx | 54 +++++++++++++++++++++------- recursive_blocks/src/Trace.ts | 33 ++++++++++++----- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index fa5565d..2ae549c 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -333,16 +333,25 @@ export function BlockEditor() { if (event.type === TraceEventType.Enter) { setHighlightedBlockId(event.blockId); - setBlockExecutionStates(prev => ({ - ...prev, - [event.blockId]: { inputs: event.inputs, output: undefined } - })); const d = depths.get(event.blockId) || 0; depths.set(event.blockId, d + 1); - // Advance PR trace panel if this block is in trace mode - advancePRPanel(event.blockId); + if (prTraceMode[event.blockId]) { + // PR trace mode: only set execution state on the first entry (original inputs) + if (d === 0) { + setBlockExecutionStates(prev => ({ + ...prev, + [event.blockId]: { inputs: event.inputs, output: undefined } + })); + } + advancePRPanel(event.blockId); + } else { + setBlockExecutionStates(prev => ({ + ...prev, + [event.blockId]: { inputs: event.inputs, output: undefined } + })); + } let shouldPause = false; if (mode === PlaybackMode.Trace) shouldPause = true; @@ -358,16 +367,25 @@ export function BlockEditor() { } else if (event.type === TraceEventType.Exit) { setHighlightedBlockId(event.blockId); setCurrentResult(event.output); - setBlockExecutionStates(prev => ({ - ...prev, - [event.blockId]: { inputs: event.inputs, output: event.output } - })); const d = depths.get(event.blockId) || 0; depths.set(event.blockId, Math.max(0, d - 1)); - // Advance PR trace panel if this block is in trace mode - advancePRPanel(event.blockId); + if (prTraceMode[event.blockId]) { + advancePRPanel(event.blockId); + // Show final output when outermost call completes + if (d === 1) { + setBlockExecutionStates(prev => ({ + ...prev, + [event.blockId]: { ...prev[event.blockId], output: event.output } + })); + } + } else { + setBlockExecutionStates(prev => ({ + ...prev, + [event.blockId]: { inputs: event.inputs, output: event.output } + })); + } let shouldPause = false; if (mode === PlaybackMode.Trace) shouldPause = true; @@ -394,6 +412,18 @@ export function BlockEditor() { for (const id of event.blockIds) { depths.delete(id); } + // Clear PR trace panels for any blocks in the cleared subtrees + setPRTraceFrames(prev => { + const next = { ...prev }; + let changed = false; + for (const id of event.blockIds) { + if (id in next) { + delete next[id]; + changed = true; + } + } + return changed ? next : prev; + }); } } diff --git a/recursive_blocks/src/Trace.ts b/recursive_blocks/src/Trace.ts index 5eacccd..abd7d47 100644 --- a/recursive_blocks/src/Trace.ts +++ b/recursive_blocks/src/Trace.ts @@ -45,14 +45,15 @@ export type PRTraceFrame = { }; /** - * Extract PR-specific animation frames from a flat trace. - * Each Enter/Exit event for the given PR block maps 1:1 to a frame. - * Descent frames show inputs flowing down; ascent frames show outputs building up. + * Build the frame sequence for a PR trace panel from a completed trace. + * Each Enter/Exit event for the given PR block produces exactly one frame. + * Descent frames show inputs flowing down, ascent frames show outputs building up. */ export function buildPRFrames(events: TraceEvent[], prBlockId: string): PRTraceFrame[] { const frames: PRTraceFrame[] = []; let depth = 0; let prevExitOutput: number | null = null; + let lastDescentFrame: PRTraceFrame | null = null; for (const e of events) { if (e.type === TraceEventType.ClearSubtree) continue; @@ -60,29 +61,43 @@ export function buildPRFrames(events: TraceEvent[], prBlockId: string): PRTraceF if (e.type === TraceEventType.Enter) { if (depth === 0) { - // New invocation — reset ascent state + // Reset state on new invocations prevExitOutput = null; + lastDescentFrame = null; } depth++; const y = e.inputs[e.inputs.length - 1]; if (y > 0) { - frames.push({ + const frame: PRTraceFrame = { xy_prime: e.inputs, xy: [...e.inputs.slice(0, -1), y - 1], hxy: null, hxy_prime: null, - }); + }; + lastDescentFrame = frame; + frames.push(frame); + } else if (lastDescentFrame) { + // Base case enter after descent, hold the last descent view + frames.push({ ...lastDescentFrame }); } else { - // Base case enter: inputs visible, results pending + // Direct call with y = 0 is an edge case, no descent to hold, so show as own level frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: null }); } } else if (e.type === TraceEventType.Exit) { depth--; const y = e.inputs[e.inputs.length - 1]; - if (y === 0) { - // Base case exit: reveal h(x,y') + if (y === 0 && lastDescentFrame) { + // Base case exit after descent, stay at last descent view, fill in h(x,y) + frames.push({ + xy_prime: lastDescentFrame.xy_prime, + xy: lastDescentFrame.xy, + hxy: e.output, + hxy_prime: null, + }); + } else if (y === 0) { + // Direct call with y = 0, just show result frames.push({ xy_prime: e.inputs, xy: null, hxy: null, hxy_prime: e.output }); } else { // Ascent: show current level with result from the level below From 01e856bede3f429ec92a4aed59d5b553087d13e2 Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 1 Apr 2026 11:16:20 -0400 Subject: [PATCH 21/31] fix more trace panel issues --- recursive_blocks/src/Block.tsx | 17 +++- recursive_blocks/src/BlockEditor.tsx | 99 ++++++++++++--------- recursive_blocks/src/BlockEditorContext.tsx | 8 ++ 3 files changed, 80 insertions(+), 44 deletions(-) diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 1c8a0b8..1039be7 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -84,7 +84,7 @@ const getMathNotation = (block: BlockData): string => { export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock, isRunning = false }: Props) { const [collapsed, setCollapsed] = React.useState(block?.collapsed ?? false); const [showInfo, setShowInfo] = React.useState(false); - const { blockExecutionStates, prTraceMode, setPRTraceMode, prTraceFrames } = useBlockEditor(); + const { blockExecutionStates, prTraceMode, setPRTraceMode, prTraceFrames, prOriginalInputs, prFinalOutputs } = useBlockEditor(); if (!block) { return Drop block here; @@ -115,6 +115,17 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on const showInputChange = isCurrentlyActive && executionState?.inputs !== undefined; const showOutputChange = isCurrentlyActive && executionState?.output !== undefined; + const isPRTraceActive = block.type === "Primitive Recursion" && prTraceMode[block.id]; + + // When PR trace panel is on, freeze in at original inputs, hide out while running + const displayInput = isPRTraceActive && prOriginalInputs[block.id] + ? prOriginalInputs[block.id] + : executionState?.inputs ?? block.latestInput; + + const displayOutput = isPRTraceActive + ? (prFinalOutputs[block.id] !== undefined ? prFinalOutputs[block.id] : undefined) + : executionState?.output ?? block.latestOutput; + const formatInput = (input: number[] | undefined) => { if (!input || input.length === 0) return '—'; return `(${input.join(', ')})`; @@ -208,7 +219,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on key={`in-${block.id}-${JSON.stringify(executionState?.inputs)}`} className={`block-io-value ${showInputChange ? 'value-changed' : ''}`} > - {formatInput(executionState?.inputs ?? block.latestInput)} + {formatInput(displayInput)}
@@ -258,7 +269,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on key={`out-${block.id}-${executionState?.output}`} className={`block-io-value ${showOutputChange ? 'value-changed' : ''}`} > - {formatOutput(executionState?.output ?? block.latestOutput)} + {formatOutput(displayOutput)}
diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 2ae549c..c8b9b21 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -39,7 +39,7 @@ enum PlaybackMode { // JSX element that represents the editor, containing a root block and the header. export function BlockEditor() { - const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates, setEditMode, prTraceMode, setPRTraceFrames } = useBlockEditor(); + const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates, setEditMode, setPRTraceFrames, setPROriginalInputs, setPRFinalOutputs } = useBlockEditor(); const [inputs, setInputs] = useState(new Array(inputCount > 0 ? inputCount : 0).fill(0)); const [highlightedBlockId, setHighlightedBlockId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); @@ -301,6 +301,8 @@ export function BlockEditor() { prCursorRef.current = {}; setBlockExecutionStates({}); setPRTraceFrames({}); + setPROriginalInputs({}); + setPRFinalOutputs({}); setCurrentResult(null); setHighlightedBlockId(null); setIsEvaluating(false); @@ -308,7 +310,7 @@ export function BlockEditor() { }; const advancePRPanel = (blockId: string) => { - if (!prTraceMode[blockId] || !prFrameListRef.current[blockId]) return; + if (!prFrameListRef.current[blockId]) return; const cursor = prCursorRef.current[blockId] ?? 0; const frames = prFrameListRef.current[blockId]; if (cursor < frames.length) { @@ -337,20 +339,18 @@ export function BlockEditor() { const d = depths.get(event.blockId) || 0; depths.set(event.blockId, d + 1); - if (prTraceMode[event.blockId]) { - // PR trace mode: only set execution state on the first entry (original inputs) - if (d === 0) { - setBlockExecutionStates(prev => ({ - ...prev, - [event.blockId]: { inputs: event.inputs, output: undefined } - })); - } + // Store original inputs on first entry for PR blocks + if (d === 0 && prFrameListRef.current[event.blockId]) { + setPROriginalInputs(prev => ({ ...prev, [event.blockId]: [...event.inputs] })); + } + + setBlockExecutionStates(prev => ({ + ...prev, + [event.blockId]: { inputs: event.inputs, output: undefined } + })); + + if (prFrameListRef.current[event.blockId]) { advancePRPanel(event.blockId); - } else { - setBlockExecutionStates(prev => ({ - ...prev, - [event.blockId]: { inputs: event.inputs, output: undefined } - })); } let shouldPause = false; @@ -371,20 +371,17 @@ export function BlockEditor() { const d = depths.get(event.blockId) || 0; depths.set(event.blockId, Math.max(0, d - 1)); - if (prTraceMode[event.blockId]) { + setBlockExecutionStates(prev => ({ + ...prev, + [event.blockId]: { inputs: event.inputs, output: event.output } + })); + + if (prFrameListRef.current[event.blockId]) { advancePRPanel(event.blockId); - // Show final output when outermost call completes + // Store final output when outermost call completes if (d === 1) { - setBlockExecutionStates(prev => ({ - ...prev, - [event.blockId]: { ...prev[event.blockId], output: event.output } - })); + setPRFinalOutputs(prev => ({ ...prev, [event.blockId]: event.output })); } - } else { - setBlockExecutionStates(prev => ({ - ...prev, - [event.blockId]: { inputs: event.inputs, output: event.output } - })); } let shouldPause = false; @@ -412,18 +409,29 @@ export function BlockEditor() { for (const id of event.blockIds) { depths.delete(id); } - // Clear PR trace panels for any blocks in the cleared subtrees - setPRTraceFrames(prev => { - const next = { ...prev }; - let changed = false; - for (const id of event.blockIds) { - if (id in next) { - delete next[id]; - changed = true; - } - } - return changed ? next : prev; - }); + + // Clear PR state for any blocks in the cleared subtrees + + // Refactor these separate PR states into one later, a single type with { frame, originalInputs, finalOutput} + // Would be easier to manage and no repetition like this + const prIdsToDelete = event.blockIds.filter(id => id in prFrameListRef.current); + if (prIdsToDelete.length > 0) { + setPRTraceFrames(prev => { + const next = { ...prev }; + for (const id of prIdsToDelete) delete next[id]; + return next; + }); + setPROriginalInputs(prev => { + const next = { ...prev }; + for (const id of prIdsToDelete) delete next[id]; + return next; + }); + setPRFinalOutputs(prev => { + const next = { ...prev }; + for (const id of prIdsToDelete) delete next[id]; + return next; + }); + } } } @@ -464,15 +472,24 @@ export function BlockEditor() { cursorRef.current = 0; finalResultRef.current = result; - // Build PR trace frames for all PR blocks in trace mode - const prBlockIds = Object.keys(prTraceMode).filter(id => prTraceMode[id]); + // Build PR trace frames for all PR blocks in the tree + const collectPRIds = (b: BlockData): string[] => { + const ids: string[] = []; + if (b.type === "Primitive Recursion") ids.push(b.id); + for (const slot of b.children) { + if (slot.block) ids.push(...collectPRIds(slot.block)); + } + return ids; + }; prFrameListRef.current = {}; prCursorRef.current = {}; - for (const id of prBlockIds) { + for (const id of collectPRIds(rootBlock)) { prFrameListRef.current[id] = buildPRFrames(events, id); prCursorRef.current[id] = 0; } setPRTraceFrames({}); + setPROriginalInputs({}); + setPRFinalOutputs({}); await advance(mode); } catch (error) { diff --git a/recursive_blocks/src/BlockEditorContext.tsx b/recursive_blocks/src/BlockEditorContext.tsx index 4e023e4..1fcb80d 100644 --- a/recursive_blocks/src/BlockEditorContext.tsx +++ b/recursive_blocks/src/BlockEditorContext.tsx @@ -24,6 +24,10 @@ const BlockEditorContext = createContext<{ setPRTraceMode: React.Dispatch>>; prTraceFrames: Record; setPRTraceFrames: React.Dispatch>>; + prOriginalInputs: Record; + setPROriginalInputs: React.Dispatch>>; + prFinalOutputs: Record; + setPRFinalOutputs: React.Dispatch>>; } | null>(null); export function BlockEditorProvider({ children }: { children: React.ReactNode }) { @@ -34,6 +38,8 @@ export function BlockEditorProvider({ children }: { children: React.ReactNode }) const [editMode, setEditMode] = useState(false); const [prTraceMode, setPRTraceMode] = useState>({}); const [prTraceFrames, setPRTraceFrames] = useState>({}); + const [prOriginalInputs, setPROriginalInputs] = useState>({}); + const [prFinalOutputs, setPRFinalOutputs] = useState>({}); return ( {children} From 2feb3e277e2cb1ca00481b686ee4d8da004c11f1 Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 1 Apr 2026 11:44:29 -0400 Subject: [PATCH 22/31] rearrange PR --- recursive_blocks/src/Block.tsx | 77 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 1039be7..6e898dc 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -224,43 +224,46 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on {!collapsed && ( - block.type === "Primitive Recursion" && prTraceMode[block.id] - ? - : ( - <> -
- {block.children.map((slot) => { - const slotDisplay = ( - - ); - - if (block.type === "Composition") { - const label = slot.name.startsWith("g") - ? g{slot.name.slice(1)} - : f; - - return ( -
-
{label}:
-
{slotDisplay}
-
- ); - } - - return
{slotDisplay}
; - })} -
- - - ) + <> +
+ {(block.type === "Primitive Recursion" ? [...block.children].reverse() : block.children).map((slot, i) => { + const slotDisplay = ( + + ); + + if (block.type === "Composition") { + const label = slot.name.startsWith("g") + ? g{slot.name.slice(1)} + : f; + + return ( +
+
{label}:
+
{slotDisplay}
+
+ ); + } + + return ( + +
{slotDisplay}
+ {block.type === "Primitive Recursion" && i === 0 && prTraceMode[block.id] && ( + + )} +
+ ); + })} +
+ + )}
From b633a58cb40319b5d24196ef8d983c77a97f07fa Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 7 Apr 2026 18:20:52 -0400 Subject: [PATCH 23/31] Update BlockEditor.tsx to remove warnings --- recursive_blocks/src/BlockEditor.tsx | 42 +++++++++------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index c8b9b21..9e39ed9 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -1,12 +1,11 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; -import { BlockData, checkForErrors, setInputCountOfBlock } from "./BlockUtil"; +import React, { useState, useCallback, useEffect, useRef } from "react"; +import { BlockData, checkForErrors, removeBlockById, setInputCountOfBlock } from "./BlockUtil"; import './Block.css'; import { Toolbar } from "./Toolbar"; import { BlockSave, deserializeBlock, serializeBlock } from "./BlockSave"; import { useBlockEditor } from "./BlockEditorContext"; import { BlockSlotDisplay } from "./BlockSlot"; import { BlockPalette } from "./BlockPalette"; -import { removeBlockById } from "./BlockUtil"; import { generateTrace, TraceEvent, TraceEventType, buildPRFrames, PRTraceFrame } from "./Trace"; export interface EditorSaveState { @@ -39,7 +38,7 @@ enum PlaybackMode { // JSX element that represents the editor, containing a root block and the header. export function BlockEditor() { - const { inputCount, setInputCount, rootBlock, setRootBlock, customBlockCount: _customBlockCount, setCustomBlockCount, setBlockExecutionStates, setEditMode, setPRTraceFrames, setPROriginalInputs, setPRFinalOutputs } = useBlockEditor(); + const { inputCount, setInputCount, rootBlock, setRootBlock, setCustomBlockCount, setBlockExecutionStates, setEditMode, setPRTraceFrames, setPROriginalInputs, setPRFinalOutputs } = useBlockEditor(); const [inputs, setInputs] = useState(new Array(inputCount > 0 ? inputCount : 0).fill(0)); const [highlightedBlockId, setHighlightedBlockId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); @@ -140,46 +139,31 @@ export function BlockEditor() { setHighlightedBlockId(null); }, [rootBlock, selectedBlockId]); - const handleAddTestData = useCallback(() => { - if (!rootBlock) { - alert("No blocks to add test data to. Create some blocks first!"); - return; - } - setRootBlock({ ...rootBlock }); - console.log("Test data added to all blocks"); - }, [rootBlock, setRootBlock]); - - const handleClearTestData = useCallback(() => { - if (!rootBlock) return; - setRootBlock({ ...rootBlock }); - console.log("Test data cleared from all blocks"); - }, [rootBlock, setRootBlock]); - useEffect(() => { - function handleKeyDown(e: KeyboardEvent) { - const isMac = navigator.platform.toUpperCase().includes("MAC"); - const ctrl = isMac ? e.metaKey : e.ctrlKey; + const handleKeyDown = (event: KeyboardEvent) => { + const isMac = navigator.userAgent.toUpperCase().includes("MAC"); + const ctrl = isMac ? event.metaKey : event.ctrlKey; if ( document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement ) return; - if ((e.key === "Backspace" || e.key === "Delete") && selectedBlockId) { - e.preventDefault(); + if ((event.key === "Backspace" || event.key === "Delete") && selectedBlockId) { + event.preventDefault(); handleDeleteSelectedBlock(); } - if (ctrl && e.shiftKey && e.key.toLowerCase() === "s") { - e.preventDefault(); + if (ctrl && event.shiftKey && event.key.toLowerCase() === "s") { + event.preventDefault(); handleSave(); } - if (ctrl && e.key.toLowerCase() === "o") { - e.preventDefault(); + if (ctrl && event.key.toLowerCase() === "o") { + event.preventDefault(); loadInputRef.current?.click(); } - } + }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); From 7a0b546d269c9cb00183a29b3b6c0894798f443e Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 7 Apr 2026 18:38:43 -0400 Subject: [PATCH 24/31] Added files for deployment --- .github/workflows/deploy.yml | 30 ++++++++++++++++++++++++++++++ recursive_blocks/vite.config.ts | 1 + 2 files changed, 31 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..2de09fd --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,30 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - run: npm install + - run: npm run build + + - uses: actions/upload-pages-artifact@v3 + with: + path: dist + + - uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/recursive_blocks/vite.config.ts b/recursive_blocks/vite.config.ts index 8b0f57b..d139ed1 100644 --- a/recursive_blocks/vite.config.ts +++ b/recursive_blocks/vite.config.ts @@ -3,5 +3,6 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ + base: "/Sunflower/", plugins: [react()], }) From ba1d5f4c61cab708b3452734cf33680610707bd1 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 7 Apr 2026 22:32:00 -0400 Subject: [PATCH 25/31] Created CNAME for domain name --- public/CNAME | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/CNAME diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..e69de29 From 209951df2745c5c9e96243a7fd2af49303821eef Mon Sep 17 00:00:00 2001 From: Hin Yan Liew Date: Wed, 8 Apr 2026 14:08:40 -0400 Subject: [PATCH 26/31] css changes --- recursive_blocks/src/Block.css | 56 ++++++++++++---------------- recursive_blocks/src/Block.tsx | 36 +++++++++--------- recursive_blocks/src/BlockConfig.ts | 11 ++++-- recursive_blocks/src/ValueEditor.tsx | 23 +++++++++++- 4 files changed, 70 insertions(+), 56 deletions(-) diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 3bad0f2..266f816 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -158,8 +158,7 @@ padding: 0.2rem 0.6rem; - /* Clean neutral background */ - background: #ffffff; + background: rgba(255, 255, 255, 0.65); /* Uniform border on all sides */ border: 1px solid rgba(0, 0, 0, 0.2); @@ -179,6 +178,7 @@ color: #444; font-size: 0.75rem; letter-spacing: 0.03em; + min-width: 1.5rem; } .block-io-value { @@ -263,32 +263,6 @@ margin-top: 0.375rem; } -/* Composition Slots Layout */ -.slots-container.composition-slots { - margin-left: 0; -} - -.composition-slot-row { - display: flex; - align-items: flex-start; - width: 100%; -} - -.composition-slot-label { - font-family: monospace; - font-size: 0.75rem; - font-weight: 600; - color: rgba(0,0,0,0.95); - padding-left: 0.2rem; - width: 0.9rem; - flex-shrink: 0; - margin-top: 0.875rem; -} - -.composition-slot-content { - flex: 1; - min-width: 0; -} .block-slot { border-radius: 1.75rem 0 0 1.75rem; @@ -399,7 +373,7 @@ font-size: 0.8125rem; font-family: ui-monospace, monospace; width: 100%; - background: rgba(255,255,255,0.55); + background: rgba(255,255,255,0.65); transition: border-color 0.15s ease, box-shadow 0.15s ease; } @@ -419,12 +393,12 @@ .value-static-name { font-weight: 500; - color: rgba(255,255,255,0.55); + color: rgba(0,0,0,0.6); } .value-static-val { - font-weight: 700; - color: rgba(255,255,255,0.85); + font-weight: 500; + color: rgba(0,0,0,0.6); } /* Palette Blocks */ @@ -762,13 +736,29 @@ label { color: var(--block-text-color); } background: rgba(223,109,38,0.12); } +/* Slot labels (primitive recursion, composition, etc) */ +.slot-label { + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: rgba(0,0,0,0.5); + margin-top: 0.85rem; + margin-bottom: -0.4rem; + padding-left: 0.875rem; +} + +@media (prefers-color-scheme: dark) { + .slot-label { color: rgba(255,255,255,0.4); } +} + /* PR Trace Panel */ .pr-trace-panel { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem 0.75rem; - margin: 0.25rem 0.75rem; + margin: 0.875rem 0.75rem 1.5rem; border: 1px solid rgba(0,0,0,0.15); border-radius: 6px; background: rgba(255,255,255,0.15); diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index 6e898dc..e1e0a2e 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -51,11 +51,20 @@ const getCustomBlockColors = (depth: number) => { const baseColor = getDepthColor(depth); return { - background: blendHexColors(baseColor, CUSTOM_BLOCK_TINT, 0.72), + background: blendHexColors(baseColor, CUSTOM_BLOCK_TINT, 0.6), border: blendHexColors(baseColor, CUSTOM_BLOCK_TINT, 0.56), }; }; +const getSlotLabel = (blockType: BlockType, slotName: string): React.ReactNode => { + if (blockType === "Composition") { + return slotName.startsWith("g") + ? g{slotName.slice(1)} + : {slotName}; + } + return slotName; +}; + const getMathNotation = (block: BlockData): string => { const n = block.inputCount ?? 0; const args = (count: number, suffix?: string) => { @@ -198,7 +207,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on T )} - {(block.children.length > 0 || (block.num_values && block.num_values.length > 0)) && ( + {(block.children.length > 0 || (block.num_values && block.num_values.length > 0) || block.type === "Zero" || block.type === "Successor") && ( )} {!block.immutable && ( @@ -225,7 +234,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on {!collapsed && ( <> -
+
{(block.type === "Primitive Recursion" ? [...block.children].reverse() : block.children).map((slot, i) => { const slotDisplay = ( ); - if (block.type === "Composition") { - const label = slot.name.startsWith("g") - ? g{slot.name.slice(1)} - : f; - - return ( -
-
{label}:
-
{slotDisplay}
-
- ); - } - return ( + {blockConfig[block.type].showSlotLabels && ( +
{getSlotLabel(block.type, slot.name)}
+ )}
{slotDisplay}
{block.type === "Primitive Recursion" && i === 0 && prTraceMode[block.id] && ( - + <> +
Trace Panel
+ + )}
); diff --git a/recursive_blocks/src/BlockConfig.ts b/recursive_blocks/src/BlockConfig.ts index a665022..b689f7a 100644 --- a/recursive_blocks/src/BlockConfig.ts +++ b/recursive_blocks/src/BlockConfig.ts @@ -77,6 +77,7 @@ export const blockConfig: Record string[]; displayName?: string; description?: string; + showSlotLabels?: boolean; }> = { "Zero": { type: "Zero" as BlockType, @@ -195,7 +196,8 @@ export const blockConfig: Record= 1 inputs. - If rightmost input is 0, returns the base case. - Otherwise, returns the recursive case evaluated where y = (rightmost input - 1), and z is the result of the previous recursive step (this block, evaluated with rightmost input decremented).` + description: `n >= 1 inputs. + If rightmost input is 0, returns the base case. + Otherwise, returns the recursive case evaluated where y = (rightmost input - 1), and z is the result of the previous recursive step (this block, evaluated with rightmost input decremented).`, + showSlotLabels: true, }, "Minimization": { type: "Minimization" as BlockType, diff --git a/recursive_blocks/src/ValueEditor.tsx b/recursive_blocks/src/ValueEditor.tsx index 3f5c374..6420be0 100644 --- a/recursive_blocks/src/ValueEditor.tsx +++ b/recursive_blocks/src/ValueEditor.tsx @@ -9,6 +9,14 @@ interface ValueEditorProps { isRunning?: boolean; } +const getDisplayOnlyNotation = (block: BlockData): { label: string; value: string } | null => { + const n = block.inputCount ?? 0; + const args = n === 0 ? "()" : `(${Array.from({ length: n }, (_, i) => `x${i + 1}`).join(", ")})`; + if (block.type === "Zero") return { label: `f${args} =`, value: "0" }; + if (block.type === "Successor") return { label: "f(x) =", value: "x + 1" }; + return null; +}; + // JSX element that exists on a JSX Block element that represents the number values of the block, making them accessible to be updated. // If the block type doesn't have values, this element is an empty div. // Block is the block this value editor is on, and onUpdate is called when a value is updated. @@ -22,7 +30,7 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP const args = Array.from({ length: inputCount }, (_, i) => `x${i + 1}`).join(", "); return { - label: `(${args}) →`, + label: `f(${args}) =`, value: `x${value}`, }; } @@ -53,7 +61,18 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP }; if (!values || values.length === 0) { - return; + const notation = getDisplayOnlyNotation(block); + if (!notation) return; + return ( +
+
+ + {notation.label} + {notation.value} + +
+
+ ); } return ( From d0d083f43254bc0ba148e9d09c2c9f95ed3277d4 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 14 Apr 2026 22:13:41 -0400 Subject: [PATCH 27/31] Updated CNAME --- public/CNAME | 1 + 1 file changed, 1 insertion(+) diff --git a/public/CNAME b/public/CNAME index e69de29..752f193 100644 --- a/public/CNAME +++ b/public/CNAME @@ -0,0 +1 @@ +sunflower.bram-hub.com \ No newline at end of file From 292be771e0a026b818e1d069d972a7e71fa84c32 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 14 Apr 2026 22:21:16 -0400 Subject: [PATCH 28/31] Updated deploy.yml --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2de09fd..034d4a5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,11 +20,11 @@ jobs: with: node-version: 18 - - run: npm install - - run: npm run build + - run: cd recursive_blocks && npm install + - run: cd recursive_blocks && npm run build - uses: actions/upload-pages-artifact@v3 with: - path: dist + path: recursive_blocks/dist - uses: actions/deploy-pages@v4 \ No newline at end of file From 58320037a111a018e4c3229dd15b1f58b75e25a7 Mon Sep 17 00:00:00 2001 From: yanga27 Date: Tue, 14 Apr 2026 22:48:54 -0400 Subject: [PATCH 29/31] Updated deployment files --- {public => recursive_blocks/public}/CNAME | 0 recursive_blocks/vite.config.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {public => recursive_blocks/public}/CNAME (100%) diff --git a/public/CNAME b/recursive_blocks/public/CNAME similarity index 100% rename from public/CNAME rename to recursive_blocks/public/CNAME diff --git a/recursive_blocks/vite.config.ts b/recursive_blocks/vite.config.ts index d139ed1..d29a0e8 100644 --- a/recursive_blocks/vite.config.ts +++ b/recursive_blocks/vite.config.ts @@ -3,6 +3,6 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ - base: "/Sunflower/", + base: "/", plugins: [react()], }) From a87c09551a10e13172ddc4d76a0b1f1556f7d4d2 Mon Sep 17 00:00:00 2001 From: hinyan17 Date: Tue, 14 Apr 2026 17:34:35 -0400 Subject: [PATCH 30/31] show formal notation and refactor notation --- recursive_blocks/src/Block.tsx | 28 +----- recursive_blocks/src/BlockEditor.tsx | 11 ++ recursive_blocks/src/Notation.ts | 144 +++++++++++++++++++++++++++ recursive_blocks/src/Toolbar.tsx | 5 + recursive_blocks/src/ValueEditor.tsx | 40 +------- 5 files changed, 167 insertions(+), 61 deletions(-) create mode 100644 recursive_blocks/src/Notation.ts diff --git a/recursive_blocks/src/Block.tsx b/recursive_blocks/src/Block.tsx index e1e0a2e..f09d0b2 100644 --- a/recursive_blocks/src/Block.tsx +++ b/recursive_blocks/src/Block.tsx @@ -7,6 +7,7 @@ import { ValueEditor } from "./ValueEditor"; import { BlockSlotDisplay } from "./BlockSlot"; import { useBlockEditor } from "./BlockEditorContext"; import { PRTracePanel } from "./PRTracePanel"; +import { getHeaderNotation } from "./Notation"; interface Props { block: BlockData | null; @@ -65,31 +66,6 @@ const getSlotLabel = (blockType: BlockType, slotName: string): React.ReactNode = return slotName; }; -const getMathNotation = (block: BlockData): string => { - const n = block.inputCount ?? 0; - const args = (count: number, suffix?: string) => { - if (count === 0) return suffix ? `(${suffix})` : "()"; - const vars = Array.from({ length: count }, (_, i) => `x${i + 1}`).join(", "); - return suffix ? `(${vars}, ${suffix})` : `(${vars})`; - }; - - switch (block.type) { - case "Zero": return `z${args(n)}`; - case "Successor": return `s(x)`; - case "Projection": { - const i = block.num_values?.find(v => v.name === "i")?.value ?? 1; - return `id[${i},${n}]${args(n)}`; - } - case "Composition": { - const m = block.num_values?.find(v => v.name === "m")?.value ?? 1; - return `C${n}[${m}]${args(n)}`; - } - case "Primitive Recursion": return `Pr[f,g]${args(n > 0 ? n - 1 : 0, "y")}`; - case "Minimization": return `Mn[f]${args(n)}`; - default: return ""; - } -}; - export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, onSelectBlock, isRunning = false }: Props) { const [collapsed, setCollapsed] = React.useState(block?.collapsed ?? false); const [showInfo, setShowInfo] = React.useState(false); @@ -182,7 +158,7 @@ export function Block({ block, onUpdate, highlightedBlockId, selectedBlockId, on
{block.name || blockConfig[block.type]?.displayName || block.type.toUpperCase()}
{block.type !== "Custom" && ( -
{getMathNotation(block)}
+
{getHeaderNotation(block)}
)}
diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index 9e39ed9..d9e34a6 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -7,6 +7,7 @@ import { useBlockEditor } from "./BlockEditorContext"; import { BlockSlotDisplay } from "./BlockSlot"; import { BlockPalette } from "./BlockPalette"; import { generateTrace, TraceEvent, TraceEventType, buildPRFrames, PRTraceFrame } from "./Trace"; +import { getFormalNotation } from "./Notation"; export interface EditorSaveState { fileType: string; @@ -122,6 +123,15 @@ export function BlockEditor() { createSaveFile(rootBlock ? serializeBlock(rootBlock) : undefined, inputs, inputCount > 0 ? inputCount : 0); }, [rootBlock, inputs, inputCount]); + const handleShowNotation = useCallback(() => { + if (!rootBlock) { + alert("No root block to format."); + return; + } + + alert(getFormalNotation(rootBlock)); + }, [rootBlock]); + const handleDeleteSelectedBlock = useCallback(() => { if (!rootBlock || !selectedBlockId) return; @@ -502,6 +512,7 @@ export function BlockEditor() { = { + "0": "\u2070", + "1": "\u00b9", + "2": "\u00b2", + "3": "\u00b3", + "4": "\u2074", + "5": "\u2075", + "6": "\u2076", + "7": "\u2077", + "8": "\u2078", + "9": "\u2079", +}; + +const SUBSCRIPT_DIGITS: Record = { + "0": "\u2080", + "1": "\u2081", + "2": "\u2082", + "3": "\u2083", + "4": "\u2084", + "5": "\u2085", + "6": "\u2086", + "7": "\u2087", + "8": "\u2088", + "9": "\u2089", +}; + +const toScriptNumber = (value: number, map: Record) => + value.toString().split("").map(ch => map[ch] ?? ch).join(""); + +const toSuperscript = (value: number) => toScriptNumber(value, SUPERSCRIPT_DIGITS); +const toSubscript = (value: number) => toScriptNumber(value, SUBSCRIPT_DIGITS); + +// Builds generic input arguments like (x1, x2, ..., xN) for a given arity +const inputArgs = (count: number) => { + if (count <= 0) return "()"; + return `(${Array.from({ length: count }, (_, i) => `x${i + 1}`).join(", ")})`; +}; + +// Returns a named child slot's formal notation (or "?" when missing) +const childFormal = (block: BlockData, slotName: string): string => { + const child = block.children.find(s => s.name === slotName)?.block; + return child ? getFormalNotation(child) : "?"; +}; + +// Returns composition's g function child slots (g1..gm) in their stored order +const compositionGSlots = (block: BlockData) => { + return block.children.filter(slot => slot.name.startsWith("g")); +}; + +export const getHeaderNotation = (block: BlockData): string => { + const n = block.inputCount ?? 0; + const args = inputArgs(n); + + switch (block.type) { + case "Zero": + return `z${args}`; + case "Successor": + return `s${args}`; + case "Projection": + return `id[n, m]${args}`; + case "Composition": { + const gSymbols = compositionGSlots(block).map((_, i) => `g${i + 1}`); + return `Cn[${["f", ...gSymbols].join(", ")}]${args}`; + } + case "Primitive Recursion": + return `Pr[f, g]${args}`; + case "Minimization": + return `Mn[f]${args}`; + case "Custom": + return `${block.name ?? "Custom"}[h]${args}`; + default: + return `?${args}`; + } +}; + +export function getFormalNotation(block: BlockData): string { + switch (block.type) { + case "Successor": + return "s"; + case "Zero": + return "z"; + case "Projection": { + const n = block.inputCount ?? 0; + const m = block.num_values?.find(v => v.name === "i")?.value ?? 1; + return `id${toSuperscript(n)}${toSubscript(m)}`; + } + case "Composition": { + const fExpr = childFormal(block, "f"); + const gExprs = compositionGSlots(block).map(slot => (slot.block ? getFormalNotation(slot.block) : "?")); + const suffix = gExprs.length > 0 ? `, ${gExprs.join(", ")}` : ""; + return `Cn[${fExpr}${suffix}]`; + } + case "Primitive Recursion": + return `Pr[${childFormal(block, "Base Case")}, ${childFormal(block, "Recursive Case")}]`; + case "Minimization": + return `Mn[${childFormal(block, "f")}]`; + case "Custom": + return childFormal(block, "Custom Function"); + default: + return "?"; + } +} + +export function getInformalNotation( + block: BlockData, + valueName?: string, + value?: number +): { label: string; value: string } | null { + if (valueName !== undefined && value !== undefined) { + if (block.type === "Projection" && valueName === "i") { + const inputCount = Math.max(block.inputCount, value, 1); + const args = Array.from({ length: inputCount }, (_, i) => `x${i + 1}`).join(", "); + + return { + label: `f(${args}) =`, + value: `x${value}`, + }; + } + + if (block.type === "Composition" && valueName === "m") { + return { + label: "arity \u2192", + value: `${value} inner block${value === 1 ? "" : "s"}`, + }; + } + + return { + label: `${valueName} \u2192`, + value: value.toString(), + }; + } + + const n = block.inputCount ?? 0; + const args = inputArgs(n); + + if (block.type === "Zero") return { label: `f${args} =`, value: "0" }; + if (block.type === "Successor") return { label: "f(x) =", value: "x + 1" }; + + return null; +} diff --git a/recursive_blocks/src/Toolbar.tsx b/recursive_blocks/src/Toolbar.tsx index 1e1b11c..069067c 100644 --- a/recursive_blocks/src/Toolbar.tsx +++ b/recursive_blocks/src/Toolbar.tsx @@ -6,6 +6,7 @@ import { MAX_INPUT_COUNT } from "./BlockEditor"; interface ToolbarProps { onSave: () => void; onLoad: (event: React.ChangeEvent) => void; + onShowNotation: () => void; loadInputRef: React.RefObject; onRun: () => void; onForceRun: () => void; @@ -26,6 +27,7 @@ interface ToolbarProps { export function Toolbar({ onSave, onLoad, + onShowNotation, loadInputRef, onRun, onForceRun, @@ -85,6 +87,9 @@ export function Toolbar({ > {editMode && !isEvaluating ? "Editing" : "Edit"} +
diff --git a/recursive_blocks/src/ValueEditor.tsx b/recursive_blocks/src/ValueEditor.tsx index 6420be0..bb7bbd4 100644 --- a/recursive_blocks/src/ValueEditor.tsx +++ b/recursive_blocks/src/ValueEditor.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { BlockData } from "./BlockUtil"; import { blockConfig } from "./BlockConfig"; import { useBlockEditor } from "./BlockEditorContext"; +import { getInformalNotation } from "./Notation"; interface ValueEditorProps { block: BlockData; @@ -9,14 +10,6 @@ interface ValueEditorProps { isRunning?: boolean; } -const getDisplayOnlyNotation = (block: BlockData): { label: string; value: string } | null => { - const n = block.inputCount ?? 0; - const args = n === 0 ? "()" : `(${Array.from({ length: n }, (_, i) => `x${i + 1}`).join(", ")})`; - if (block.type === "Zero") return { label: `f${args} =`, value: "0" }; - if (block.type === "Successor") return { label: "f(x) =", value: "x + 1" }; - return null; -}; - // JSX element that exists on a JSX Block element that represents the number values of the block, making them accessible to be updated. // If the block type doesn't have values, this element is an empty div. // Block is the block this value editor is on, and onUpdate is called when a value is updated. @@ -24,30 +17,6 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP const [values, setValues] = useState(block.num_values ?? []); const { editMode } = useBlockEditor(); - const formatStaticValue = (valueName: string, value: number) => { - if (block.type === "Projection" && valueName === "i") { - const inputCount = Math.max(block.inputCount, value, 1); - const args = Array.from({ length: inputCount }, (_, i) => `x${i + 1}`).join(", "); - - return { - label: `f(${args}) =`, - value: `x${value}`, - }; - } - - if (block.type === "Composition" && valueName === "m") { - return { - label: "arity →", - value: `${value} inner block${value === 1 ? "" : "s"}`, - }; - } - - return { - label: `${valueName} →`, - value: value.toString(), - }; - }; - useEffect(() => { if (JSON.stringify(values) !== JSON.stringify(block.num_values)) { onUpdate({ ...block, num_values: values, depth: block.depth || 0 }); @@ -61,7 +30,7 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP }; if (!values || values.length === 0) { - const notation = getDisplayOnlyNotation(block); + const notation = getInformalNotation(block); if (!notation) return; return (
@@ -76,9 +45,10 @@ export function ValueEditor({ block, onUpdate, isRunning = false }: ValueEditorP } return ( -
+
{values.map((val, index) => { - const staticValue = formatStaticValue(val.name, val.value); + const staticValue = getInformalNotation(block, val.name, val.value); + if (!staticValue) return null; return (
From 8e532551d3fd663235992955f7e1d0aed7da01d1 Mon Sep 17 00:00:00 2001 From: hinyan17 Date: Wed, 15 Apr 2026 12:29:14 -0400 Subject: [PATCH 31/31] formal notation panel --- recursive_blocks/src/Block.css | 52 ++++++++++++++++++++++++++++ recursive_blocks/src/BlockEditor.tsx | 23 ++++++++++-- recursive_blocks/src/Notation.ts | 3 +- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/recursive_blocks/src/Block.css b/recursive_blocks/src/Block.css index 266f816..90c00ed 100644 --- a/recursive_blocks/src/Block.css +++ b/recursive_blocks/src/Block.css @@ -794,3 +794,55 @@ label { color: var(--block-text-color); } text-align: center; background: rgba(255,255,255,0.25); } + +/* Notation popup */ +.notation-popup-overlay { + position: fixed; + inset: 0; + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.45); +} + +.notation-popup-panel { + width: min(680px, calc(100vw - 2rem)); + max-height: calc(100vh - 2rem); + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem; + border-radius: 10px; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #ffffff; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); +} + +.notation-popup-title { + font-size: 1rem; + font-weight: 700; +} + +.notation-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.notation-popup-body { + margin: 0; + padding: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 6px; + background: #f7f7f7; + overflow: auto; + font-size: 0.9rem; + white-space: pre-wrap; + word-break: break-word; +} + +.notation-popup-close { + flex-shrink: 0; +} diff --git a/recursive_blocks/src/BlockEditor.tsx b/recursive_blocks/src/BlockEditor.tsx index d9e34a6..f93a23d 100644 --- a/recursive_blocks/src/BlockEditor.tsx +++ b/recursive_blocks/src/BlockEditor.tsx @@ -45,6 +45,7 @@ export function BlockEditor() { const [selectedBlockId, setSelectedBlockId] = useState(null); const [currentResult, setCurrentResult] = useState(null); + const [notationPopupText, setNotationPopupText] = useState(null); const [isEvaluating, setIsEvaluating] = useState(false); const [evaluationSpeed, setEvaluationSpeed] = useState(2); const [paused, setPaused] = useState(false); @@ -125,13 +126,17 @@ export function BlockEditor() { const handleShowNotation = useCallback(() => { if (!rootBlock) { - alert("No root block to format."); + setNotationPopupText("No root block to format."); return; } - alert(getFormalNotation(rootBlock)); + setNotationPopupText(getFormalNotation(rootBlock)); }, [rootBlock]); + const handleCloseNotationPopup = useCallback(() => { + setNotationPopupText(null); + }, []); + const handleDeleteSelectedBlock = useCallback(() => { if (!rootBlock || !selectedBlockId) return; @@ -553,6 +558,20 @@ export function BlockEditor() {

+ + {notationPopupText !== null && ( +
+
+
+
Formal Notation
+ +
+
{notationPopupText}
+
+
+ )} ); } diff --git a/recursive_blocks/src/Notation.ts b/recursive_blocks/src/Notation.ts index 55ff069..68e0f2c 100644 --- a/recursive_blocks/src/Notation.ts +++ b/recursive_blocks/src/Notation.ts @@ -1,7 +1,6 @@ import { BlockData } from "./BlockUtil"; -// Use unicode for sub and superscripts in the alert -// When a UI panel is made for this, maybe handle them with react, but this is simpler +// Use unicode for sub and superscripts. Could be handled with react, but this is simpler const SUPERSCRIPT_DIGITS: Record = { "0": "\u2070", "1": "\u00b9",