diff --git a/src/components/Workspace.tsx b/src/components/Workspace.tsx index 30ca74a..9ace9da 100644 --- a/src/components/Workspace.tsx +++ b/src/components/Workspace.tsx @@ -20,6 +20,7 @@ import { OperationTags, outputTypes, IOTypes, + ConfigValues, } from "@/operations/types"; import { operations } from "@/operations/operations"; import { @@ -87,21 +88,32 @@ const Workspace: React.FC = () => { state.edges ); const sortedConnectedNodes = topologicalSort(connectedNodes, state.edges); - const lastConnectedNode = sortedConnectedNodes[sortedConnectedNodes.length - 1]; - const lastNodeY = lastConnectedNode - ? lastConnectedNode.position.y + - (lastConnectedNode.measured?.height || 0) - : 0; - const lastNodeX = lastConnectedNode ? lastConnectedNode.position.x : 250; - const newNode: Node = { id: generateShortId(operation.id), type: "custom", - data: operation, - position: { x: lastNodeX, y: lastNodeY + 80 }, + data: { + ...operation, + onConfigChange: (newConfig: ConfigValues) => { + if (operation.funcBuilder) { + dispatch({ + type: "UPDATE_NODE_CONFIG", + nodeId: newNode.id, + config: newConfig, + }); + } + }, + }, + position: { + x: lastConnectedNode ? lastConnectedNode.position.x : 250, + y: lastConnectedNode + ? lastConnectedNode.position.y + + (lastConnectedNode.measured?.height || 0) + + 80 + : 10, + }, }; if (operation.tags.includes(OperationTags.IO)) { @@ -228,7 +240,7 @@ const Workspace: React.FC = () => { }, [state.nodes, state.edges, state.selectedNodeId, state.dirtyNodes]); const debouncedCalculate = useMemo( - () => debounce(calculate, 250), + () => debounce(calculate, 100), [calculate] ); diff --git a/src/components/flow/FlowGraph.tsx b/src/components/flow/FlowGraph.tsx index 8873860..ebb5253 100644 --- a/src/components/flow/FlowGraph.tsx +++ b/src/components/flow/FlowGraph.tsx @@ -72,6 +72,7 @@ export const FlowGraph = React.memo( onNodesDelete={handleNodesDelete} onNodeClick={(_, node) => onNodeClick?.(node.id)} fitView + proOptions={{ hideAttribution: true }} > diff --git a/src/nodes/customNode.tsx b/src/nodes/customNode.tsx index fe162a7..e54cdfc 100644 --- a/src/nodes/customNode.tsx +++ b/src/nodes/customNode.tsx @@ -16,14 +16,14 @@ const propsAreEqual = ( prevProps.id === nextProps.id && prevProps.selected === nextProps.selected && prevProps.data.value === nextProps.data.value && - prevProps.data.outputValues === nextProps.data.outputValues + prevProps.data.outputValues === nextProps.data.outputValues && + prevProps.data.configValues === nextProps.data.configValues ); }; const CustomNode = ({ id, data, selected }: CustomNodeProps) => { const input_length = data.inputs ? Object.keys(data.inputs).length : 0; const output_length = data.outputs ? Object.keys(data.outputs).length : 0; - const inputHandles = useMemo(() => { if (!data.inputs) return null; return Object.entries(data.inputs).map(([key, type], index) => ( @@ -75,6 +75,61 @@ const CustomNode = ({ id, data, selected }: CustomNodeProps) => { )); }, [data.outputs, output_length]); + const configInputs = useMemo(() => { + if (!data.configValues) return null; + return ( +
+ {Object.entries(data.configValues).map(([key, value]) => ( +
+ + {typeof value === "boolean" ? ( + { + if (data.onConfigChange) { + data.onConfigChange({ + ...data.configValues, + [key]: e.target.checked, + }); + } + }} + /> + ) : typeof value === "number" ? ( + { + if (data.onConfigChange) { + data.onConfigChange({ + ...data.configValues, + [key]: Number(e.target.value), + }); + } + }} + className="w-16 px-1" + /> + ) : ( + { + if (data.onConfigChange) { + data.onConfigChange({ + ...data.configValues, + [key]: e.target.value, + }); + } + }} + className="w-24 px-1" + /> + )} +
+ ))} +
+ ); + }, [data]); + const nodeClassName = useMemo( () => `p-4 border rounded shadow-md bg-white items-center cursor-pointer hover:bg-gray-50 ${ @@ -91,6 +146,7 @@ const CustomNode = ({ id, data, selected }: CustomNodeProps) => { {inputHandles}
{data.value?.slice(0, 30) || ""}
+ {configInputs} {outputHandles} ); diff --git a/src/operations/string/encoding.ts b/src/operations/string/encoding.ts index c897f2e..6b5d473 100644 --- a/src/operations/string/encoding.ts +++ b/src/operations/string/encoding.ts @@ -1,4 +1,9 @@ -import { Operation, OperationTags, IOTypes } from "@/operations/types"; +import { + Operation, + OperationTags, + IOTypes, + ConfigValues, +} from "@/operations/types"; import { encode_base16, decode_base16, @@ -10,6 +15,7 @@ import { decode_base64_standard, encode_base64_url, decode_base64_url, + build_base16_encoder, } from "wasm"; import { validateOutputTypeStringToString } from "@/operations/string/utils"; @@ -19,6 +25,17 @@ export const Base16Encode: Operation = { description: "Encodes the input string to Base16 (hexadecimal)", link: "https://en.wikipedia.org/wiki/Hexadecimal", value: "", + configValues: { + uppercase: false, + }, + funcBuilder: (config?: ConfigValues) => { + const builtFunction = build_base16_encoder(JSON.stringify(config || {})); + return validateOutputTypeStringToString( + (input: string) => builtFunction(input), + [IOTypes.Text], + [IOTypes.Text] + ); + }, func: validateOutputTypeStringToString( encode_base16, [IOTypes.Text], diff --git a/src/operations/types.ts b/src/operations/types.ts index 5a6d557..46add47 100644 --- a/src/operations/types.ts +++ b/src/operations/types.ts @@ -15,6 +15,10 @@ export enum IOTypes { Binary = "Binary", } +export type ConfigValues = { + [key: string]: string | number | boolean; +}; + export type outputTypes = string | number | number[] | string[]; export type OperationFunction = (...args: outputTypes[]) => outputTypes[]; @@ -24,11 +28,14 @@ export interface Operation { description: string; value: string; outputValues?: { [key: string]: outputTypes }; + configValues?: ConfigValues; id: string; + funcBuilder?: (configValues?: ConfigValues) => OperationFunction; func: OperationFunction; tags: OperationTags[]; inputs: { [key: string]: IOTypes }; outputs: { [key: string]: IOTypes }; link?: string; + onConfigChange?: (newConfig: ConfigValues) => void; [key: string]: unknown; } diff --git a/src/state/graphReducer.ts b/src/state/graphReducer.ts index 47aaa35..5f8806b 100644 --- a/src/state/graphReducer.ts +++ b/src/state/graphReducer.ts @@ -1,4 +1,4 @@ -import { Operation } from "@/operations/types"; +import { ConfigValues, Operation } from "@/operations/types"; import { Edge, Node, @@ -36,7 +36,8 @@ export type GraphAction = | { type: "APPLY_NODE_CHANGES"; changes: NodeChange[] } | { type: "APPLY_EDGE_CHANGES"; changes: EdgeChange[] } | { type: "SET_EDGES_AND_NODES"; edges: Edge[]; nodes: Node[] } - | { type: "SET_NODES_AND_CLEAR_DIRTY"; nodes: Node[] }; + | { type: "SET_NODES_AND_CLEAR_DIRTY"; nodes: Node[] } + | { type: "UPDATE_NODE_CONFIG"; nodeId: string; config: ConfigValues }; export const initialGraphState: GraphState = { nodes: [], @@ -157,6 +158,34 @@ export const graphReducer = ( dirtyNodes: new Set(), }; + case "UPDATE_NODE_CONFIG": { + const node = state.nodes.find((n) => n.id === action.nodeId); + const operation = node?.data as Operation; + const funcBuilder = operation?.funcBuilder; + if (typeof funcBuilder !== "function") return state; + + const newNodes = state.nodes.map((node) => + node.id === action.nodeId + ? { + ...node, + data: { + ...node.data, + configValues: action.config, + func: funcBuilder(action.config), + }, + } + : node + ); + + const affectedNodes = getDownstreamNodes(state.edges, action.nodeId); + affectedNodes.push(action.nodeId); + return { + ...state, + nodes: newNodes, + dirtyNodes: new Set([...state.dirtyNodes, ...affectedNodes]), + }; + } + default: return state; } diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 2824748..794ad65 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -25,6 +25,8 @@ base85 = "1.0.0" md-5 = "0.10.6" sha1 = "0.10.6" sha2 = "0.10.6" +serde_json = "1.0.138" +js-sys = "0.3.77" [dev-dependencies] wasm-bindgen-test = "0.3.34" diff --git a/wasm/src/string_ops/encoding.rs b/wasm/src/string_ops/encoding.rs index 5e0c8ab..5b2e21a 100644 --- a/wasm/src/string_ops/encoding.rs +++ b/wasm/src/string_ops/encoding.rs @@ -1,14 +1,34 @@ use wasm_bindgen::prelude::*; use base64::{Engine as _, engine::general_purpose}; -use data_encoding::{HEXLOWER as BASE16, BASE32}; +use data_encoding::{HEXLOWER as BASE16, BASE32, HEXUPPER}; use base85::{encode, decode}; use crate::utils::process_lines; +use serde_json::Value; +use wasm_bindgen::JsValue; #[wasm_bindgen] pub fn encode_base16(s: &str) -> Vec { vec![process_lines(s, |line| BASE16.encode(line.as_bytes()))] } +#[wasm_bindgen] +pub fn build_base16_encoder(config: &str) -> Result { + let config: Value = serde_json::from_str(config) + .map_err(|e| JsValue::from_str(&format!("Invalid config JSON: {}", e)))?; + + let uppercase = config.get("uppercase") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let encoder = if uppercase { HEXUPPER } else { BASE16 }; + + let closure: Closure Vec> = Closure::new(move |s: String| { + vec![process_lines(&s, |line| encoder.encode(line.as_bytes()))] + }); + + Ok(closure.into_js_value().unchecked_into::()) +} + #[wasm_bindgen] pub fn decode_base16(s: &str) -> Vec { vec![process_lines(s, |line| {