UNPKG

@copilotkit/react-core

Version:

<div align="center"> <a href="https://copilotkit.ai" target="_blank"> <img src="https://github.com/copilotkit/copilotkit/raw/main/assets/banner.png" alt="CopilotKit Logo"> </a>

203 lines (172 loc) 5.3 kB
import { randomId } from "@copilotkit/shared"; import { useCallback, useReducer } from "react"; export type TreeNodeId = string; export interface TreeNode { id: TreeNodeId; value: string; children: TreeNode[]; parentId?: TreeNodeId; categories: Set<string>; } export type Tree = TreeNode[]; export interface UseTreeReturn { tree: Tree; addElement: (value: string, categories: string[], parentId?: TreeNodeId) => TreeNodeId; printTree: (categories: string[]) => string; removeElement: (id: TreeNodeId) => void; } const findNode = (nodes: Tree, id: TreeNodeId): TreeNode | undefined => { for (const node of nodes) { if (node.id === id) { return node; } const result = findNode(node.children, id); if (result) { return result; } } return undefined; }; const removeNode = (nodes: Tree, id: TreeNodeId): Tree => { return nodes.reduce((result: Tree, node) => { if (node.id !== id) { const newNode = { ...node, children: removeNode(node.children, id) }; result.push(newNode); } return result; }, []); }; const addNode = (nodes: Tree, newNode: TreeNode, parentId?: TreeNodeId): Tree => { if (!parentId) { return [...nodes, newNode]; } return nodes.map((node) => { if (node.id === parentId) { return { ...node, children: [...node.children, newNode] }; } else if (node.children.length) { return { ...node, children: addNode(node.children, newNode, parentId) }; } return node; }); }; const treeIndentationRepresentation = (index: number, indentLevel: number): string => { if (indentLevel === 0) { return (index + 1).toString(); } else if (indentLevel === 1) { return String.fromCharCode(65 + index); // 65 is the ASCII value for 'A' } else if (indentLevel === 2) { return String.fromCharCode(97 + index); // 97 is the ASCII value for 'a' } else { return "-"; } }; const printNode = (node: TreeNode, prefix = "", indentLevel = 0): string => { const indent = " ".repeat(3).repeat(indentLevel); const prefixPlusIndentLength = prefix.length + indent.length; const subsequentLinesPrefix = " ".repeat(prefixPlusIndentLength); const valueLines = node.value.split("\n"); const outputFirstLine = `${indent}${prefix}${valueLines[0]}`; const outputSubsequentLines = valueLines .slice(1) .map((line) => `${subsequentLinesPrefix}${line}`) .join("\n"); let output = `${outputFirstLine}\n`; if (outputSubsequentLines) { output += `${outputSubsequentLines}\n`; } const childPrePrefix = " ".repeat(prefix.length); node.children.forEach( (child, index) => (output += printNode( child, `${childPrePrefix}${treeIndentationRepresentation(index, indentLevel + 1)}. `, indentLevel + 1, )), ); return output; }; // Action types type Action = | { type: "ADD_NODE"; value: string; parentId?: string; id: string; categories: string[]; } | { type: "REMOVE_NODE"; id: string }; // Reducer function function treeReducer(state: Tree, action: Action): Tree { switch (action.type) { case "ADD_NODE": { const { value, parentId, id: newNodeId } = action; const newNode: TreeNode = { id: newNodeId, value, children: [], categories: new Set(action.categories), }; try { return addNode(state, newNode, parentId); } catch (error) { console.error(`Error while adding node with id ${newNodeId}: ${error}`); return state; } } case "REMOVE_NODE": return removeNode(state, action.id); default: return state; } } // useTree hook const useTree = (): UseTreeReturn => { const [tree, dispatch] = useReducer(treeReducer, []); const addElement = useCallback( (value: string, categories: string[], parentId?: string): TreeNodeId => { const newNodeId = randomId(); // Generate new ID outside of dispatch dispatch({ type: "ADD_NODE", value, parentId, id: newNodeId, categories: categories, }); return newNodeId; // Return the new ID }, [], ); const removeElement = useCallback((id: TreeNodeId): void => { dispatch({ type: "REMOVE_NODE", id }); }, []); const printTree = useCallback( (categories: string[]): string => { const categoriesSet = new Set(categories); let output = ""; tree.forEach((node, index) => { // if the node does not have any of the desired categories, continue to the next node if (!setsHaveIntersection(categoriesSet, node.categories)) { return; } // add a new line before each node except the first one if (index !== 0) { output += "\n"; } output += printNode(node, `${treeIndentationRepresentation(index, 0)}. `); }); return output; }, [tree], ); return { tree, addElement, printTree, removeElement }; }; export default useTree; function setsHaveIntersection<T>(setA: Set<T>, setB: Set<T>): boolean { const [smallerSet, largerSet] = setA.size <= setB.size ? [setA, setB] : [setB, setA]; for (let item of smallerSet) { if (largerSet.has(item)) { return true; } } return false; }