diff --git a/README.md b/README.md index ad69657..bf82a68 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The project is organized into autonomous modules: - **flowsint-types**: Pydantic models and type definitions - **flowsint-transforms**: Transform modules, scanning logic, and tools - **flowsint-api**: FastAPI server, API routes, and schemas only -- **flowsint-app**: Frontend application (unchanged) +- **flowsint-app**: Frontend application ### Module dependencies diff --git a/flowsint-api/app/api/routes/chat.py b/flowsint-api/app/api/routes/chat.py index 8808d76..95a28a6 100644 --- a/flowsint-api/app/api/routes/chat.py +++ b/flowsint-api/app/api/routes/chat.py @@ -26,20 +26,15 @@ def clean_context(context: List[Dict]) -> List[Dict]: if isinstance(item, dict): # Create a copy and remove unwanted keys cleaned_item = item["data"].copy() - # Remove top-level keys cleaned_item.pop("id", None) cleaned_item.pop("sketch_id", None) - # Remove from data if it exists if "data" in cleaned_item and isinstance(cleaned_item["data"], dict): cleaned_item["data"].pop("sketch_id", None) - # Remove measured/dimensions cleaned_item.pop("measured", None) - cleaned.append(cleaned_item) - print(cleaned) return cleaned diff --git a/flowsint-app/src/renderer/index.html b/flowsint-app/src/renderer/index.html index c41e657..153b424 100644 --- a/flowsint-app/src/renderer/index.html +++ b/flowsint-app/src/renderer/index.html @@ -1,11 +1,11 @@ - + diff --git a/flowsint-app/src/renderer/public/icons/email.svg b/flowsint-app/src/renderer/public/icons/email.svg index dee1304..984fd0c 100644 --- a/flowsint-app/src/renderer/public/icons/email.svg +++ b/flowsint-app/src/renderer/public/icons/email.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flowsint-app/src/renderer/src/components/flows/editor.tsx b/flowsint-app/src/renderer/src/components/flows/editor.tsx index 535b36b..68074f2 100644 --- a/flowsint-app/src/renderer/src/components/flows/editor.tsx +++ b/flowsint-app/src/renderer/src/components/flows/editor.tsx @@ -22,7 +22,7 @@ import TransformNode from "./transform-node" import TypeNode from "./type-node" import { type TransformNodeData } from "@/types/transform" import { FlowControls } from "./controls" -import { getDagreLayoutedElements } from "@/lib/utils" +import { getFlowDagreLayoutedElements } from "@/lib/utils" import { toast } from "sonner" import { SaveModal } from "./save-modal" import { useConfirm } from "@/components/use-confirm-dialog" @@ -302,7 +302,7 @@ const FlowEditor = memo(({ initialEdges, initialNodes, theme, flow }: FlowEditor const onLayout = useCallback(() => { // Wait for nodes to be measured before running layout setTimeout(() => { - const layouted = getDagreLayoutedElements( + const layouted = getFlowDagreLayoutedElements( nodes.map(node => ({ ...node, measured: { diff --git a/flowsint-app/src/renderer/src/components/graphs/graph-viewer.tsx b/flowsint-app/src/renderer/src/components/graphs/graph-viewer.tsx index 015fdbe..3d48ae9 100644 --- a/flowsint-app/src/renderer/src/components/graphs/graph-viewer.tsx +++ b/flowsint-app/src/renderer/src/components/graphs/graph-viewer.tsx @@ -151,17 +151,14 @@ const GraphViewer: React.FC = ({ allowLasso = false, minimap = false }) => { - const [currentZoom, setCurrentZoom] = useState({ - k: 1, - x: 1, - y: 1 - }); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const isLassoActive = useGraphControls(s => s.isLassoActive) // Hover highlighting state const [highlightNodes, setHighlightNodes] = useState>(new Set()); const [highlightLinks, setHighlightLinks] = useState>(new Set()); const [hoverNode, setHoverNode] = useState(null); + const zoomRef = useRef({ k: 1, x: 0, y: 0 }); + const [zoomState, setZoomState] = useState({ k: 1, x: 0, y: 0 }); // Store references const graphRef = useRef(); @@ -211,6 +208,11 @@ const GraphViewer: React.FC = ({ } }, [nodes, showIcons]); + const handleZoom = useCallback((zoom: any) => { + zoomRef.current = zoom; + setZoomState(zoom); + }, []); + // Optimized graph initialization callback const initializeGraph = useCallback((graphInstance: any) => { if (!graphInstance) return; @@ -315,8 +317,8 @@ const GraphViewer: React.FC = ({ // Memoized rendering check const shouldUseSimpleRendering = useMemo(() => - nodes.length > CONSTANTS.NODE_COUNT_THRESHOLD || currentZoom.k < 1.5 - , [nodes.length, currentZoom]); + nodes.length > CONSTANTS.NODE_COUNT_THRESHOLD || zoomState.k < 1.5 + , [nodes.length, zoomState.k]); // Memoized graph data transformation with proper memoization dependencies const graphData = useMemo(() => { @@ -350,12 +352,11 @@ const GraphViewer: React.FC = ({ const group = edgeGroups.get(key)!; const groupIndex = group.indexOf(edge); const groupSize = group.length; - const curve = groupSize > 1 ? (groupIndex - (groupSize - 1) / 2) * 0.2 : 0; - + const curvature = groupSize > 1 ? (groupIndex - (groupSize - 1) / 2) * 0.2 : 0; return { ...edge, edgeLabel: edge.label, - curve, + curvature, groupIndex, groupSize }; @@ -421,7 +422,7 @@ const GraphViewer: React.FC = ({ if (!showLabels) return new Set(); // Find the appropriate layer for current zoom - const currentLayer = CONSTANTS.LABEL_LAYERS.find(layer => currentZoom.k >= layer.minZoom); + const currentLayer = CONSTANTS.LABEL_LAYERS.find(layer => zoomState.k >= layer.minZoom); if (!currentLayer) return new Set(); // Sort nodes by weight (number of connections) in descending order @@ -440,7 +441,7 @@ const GraphViewer: React.FC = ({ .slice(0, currentLayer.maxNodes); return new Set(visibleNodes.map(node => node.id)); - }, [graphData.nodes, currentZoom, showLabels]); + }, [graphData.nodes, zoomState.k, showLabels]); // Event handlers with proper memoization const handleNodeClick = useCallback((node: any, event: MouseEvent) => { @@ -519,7 +520,6 @@ const GraphViewer: React.FC = ({ if (node) { const weight = node.neighbors?.length || 0; const label = node.nodeLabel || node.label || node.id; - // Position tooltip above the node using the graph's coordinate conversion if (graphRef.current) { try { @@ -582,12 +582,10 @@ const GraphViewer: React.FC = ({ if (link) { // Add the hovered link newHighlightLinks.add(`${link.source}-${link.target}`); - // Add connected nodes newHighlightNodes.add(link.source.id); newHighlightNodes.add(link.target.id); } - setHoverNode(null); setHighlightNodes(newHighlightNodes); setHighlightLinks(newHighlightLinks); @@ -600,7 +598,6 @@ const GraphViewer: React.FC = ({ const isHighlighted = highlightNodes.has(node.id) || isSelected(node.id); const hasAnyHighlight = highlightNodes.size > 0 || highlightLinks.size > 0; const isHovered = hoverNode === node.id || (isCurrent(node.id)); - // Draw highlight ring for highlighted nodes if (isHighlighted) { ctx.beginPath(); @@ -608,21 +605,17 @@ const GraphViewer: React.FC = ({ ctx.fillStyle = isHovered ? GRAPH_COLORS.NODE_HIGHLIGHT_HOVER : GRAPH_COLORS.NODE_HIGHLIGHT_DEFAULT; ctx.fill(); } - // Set node color based on highlight state if (hasAnyHighlight) { ctx.fillStyle = isHighlighted ? node.nodeColor : `${node.nodeColor}7D`; } else { ctx.fillStyle = node.nodeColor; } - ctx.beginPath(); ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); ctx.fill(); - // Early exit for simple rendering if (shouldUseSimpleRendering) return; - // Optimized icon rendering with cached images if (showIcons && node.nodeType) { const cachedImage = imageCache.get(node.nodeType); @@ -634,7 +627,6 @@ const GraphViewer: React.FC = ({ } } } - // Optimized label rendering with layered display if (showLabels && globalScale > 3) { const label = truncateText(node.nodeLabel || node.label || node.id, 58); @@ -672,7 +664,6 @@ const GraphViewer: React.FC = ({ let strokeStyle: string; let lineWidth: number; let fillStyle: string; - if (isHighlighted) { strokeStyle = GRAPH_COLORS.LINK_HIGHLIGHTED; fillStyle = GRAPH_COLORS.LINK_HIGHLIGHTED; @@ -686,10 +677,25 @@ const GraphViewer: React.FC = ({ fillStyle = GRAPH_COLORS.LINK_DEFAULT; lineWidth = CONSTANTS.LINK_WIDTH * (forceSettings.linkWidth.value / 5); } - // Draw connection line + // Draw connection line (use quadratic curve if curvature present) + const curvature: number = link.curvature || 0; + const dx = end.x - start.x; + const dy = end.y - start.y; + const distance = Math.sqrt(dx * dx + dy * dy) || 1; + const midX = (start.x + end.x) * 0.5; + const midY = (start.y + end.y) * 0.5; + const normX = -dy / distance; + const normY = dx / distance; + const offset = curvature * distance; + const ctrlX = midX + normX * offset; + const ctrlY = midY + normY * offset; ctx.beginPath(); ctx.moveTo(start.x, start.y); - ctx.lineTo(end.x, end.y); + if (curvature !== 0) { + ctx.quadraticCurveTo(ctrlX, ctrlY, end.x, end.y); + } else { + ctx.lineTo(end.x, end.y); + } ctx.strokeStyle = strokeStyle; ctx.lineWidth = lineWidth; ctx.stroke(); @@ -697,27 +703,38 @@ const GraphViewer: React.FC = ({ const arrowLength = forceSettings.linkDirectionalArrowLength.value; if (arrowLength && arrowLength > 0) { const arrowRelPos = forceSettings.linkDirectionalArrowRelPos.value || 1; - // Calculate arrow position along the link - let arrowX = start.x + (end.x - start.x) * arrowRelPos; - let arrowY = start.y + (end.y - start.y) * arrowRelPos; - // If arrow is at the target node (arrowRelPos = 1), offset it to be at the node's edge - if (arrowRelPos === 1) { - const dx = end.x - start.x; - const dy = end.y - start.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance > 0) { - // Calculate target node size (same as in renderNode function) - const targetNodeSize = (end.nodeSize || CONSTANTS.NODE_DEFAULT_SIZE) * (forceSettings.nodeSize.value / 100 + 0.4); - // Calculate offset to place arrow at node edge - const offset = targetNodeSize / distance; - arrowX = end.x - dx * offset; - arrowY = end.y - dy * offset; + // Helper to get point and tangent along straight/curved link + const bezierPoint = (t: number) => { + if (curvature === 0) { + return { x: start.x + dx * t, y: start.y + dy * t }; } + const oneMinusT = 1 - t; + return { + x: oneMinusT * oneMinusT * start.x + 2 * oneMinusT * t * ctrlX + t * t * end.x, + y: oneMinusT * oneMinusT * start.y + 2 * oneMinusT * t * ctrlY + t * t * end.y, + }; + }; + const bezierTangent = (t: number) => { + if (curvature === 0) { + return { x: dx, y: dy }; + } + const oneMinusT = 1 - t; + return { + x: 2 * oneMinusT * (ctrlX - start.x) + 2 * t * (end.x - ctrlX), + y: 2 * oneMinusT * (ctrlY - start.y) + 2 * t * (end.y - ctrlY), + }; + }; + let t = arrowRelPos; + let { x: arrowX, y: arrowY } = bezierPoint(t); + if (arrowRelPos === 1) { + const tan = bezierTangent(0.99); + const tanLen = Math.hypot(tan.x, tan.y) || 1; + const targetNodeSize = (end.nodeSize || CONSTANTS.NODE_DEFAULT_SIZE) * (forceSettings.nodeSize.value / 100 + 0.4); + arrowX = end.x - (tan.x / tanLen) * targetNodeSize; + arrowY = end.y - (tan.y / tanLen) * targetNodeSize; } - // Calculate arrow direction - const dx = end.x - start.x; - const dy = end.y - start.y; - const angle = Math.atan2(dy, dx); + const tan = bezierTangent(t); + const angle = Math.atan2(tan.y, tan.x); // Draw arrow head ctx.save(); ctx.translate(arrowX, arrowY); @@ -735,12 +752,24 @@ const GraphViewer: React.FC = ({ if (shouldUseSimpleRendering || !link.label) return; // Only show labels for highlighted links when there's any highlighting if (isHighlighted) { - // Calculate label position and angle - tempPos.x = (start.x + end.x) * 0.5; - tempPos.y = (start.y + end.y) * 0.5; - const dx = end.x - start.x; - const dy = end.y - start.y; - let textAngle = Math.atan2(dy, dx); + // Calculate label position and angle along straight/curved link + let textAngle: number; + if ((link.curvature || 0) !== 0) { + // Bezier midpoint and tangent at t=0.5 + const t = 0.5; + const oneMinusT = 1 - t; + tempPos.x = oneMinusT * oneMinusT * start.x + 2 * oneMinusT * t * ctrlX + t * t * end.x; + tempPos.y = oneMinusT * oneMinusT * start.y + 2 * oneMinusT * t * ctrlY + t * t * end.y; + const tx = 2 * oneMinusT * (ctrlX - start.x) + 2 * t * (end.x - ctrlX); + const ty = 2 * oneMinusT * (ctrlY - start.y) + 2 * t * (end.y - ctrlY); + textAngle = Math.atan2(ty, tx); + } else { + tempPos.x = (start.x + end.x) * 0.5; + tempPos.y = (start.y + end.y) * 0.5; + const sdx = end.x - start.x; + const sdy = end.y - start.y; + textAngle = Math.atan2(sdy, sdx); + } // Flip text for readability if (textAngle > CONSTANTS.HALF_PI || textAngle < -CONSTANTS.HALF_PI) { textAngle += textAngle > 0 ? -CONSTANTS.PI : CONSTANTS.PI; @@ -772,7 +801,6 @@ const GraphViewer: React.FC = ({ // Restart simulation when settings change (debounced) useEffect(() => { let settingsTimeout: number | undefined; - const restartSimulation = () => { if (graphRef.current && isGraphReadyRef.current) { if (settingsTimeout) clearTimeout(settingsTimeout); @@ -781,10 +809,8 @@ const GraphViewer: React.FC = ({ }, 100) as any; // Debounce settings changes } }; - // Restart simulation when force settings change restartSimulation(); - return () => { if (settingsTimeout) clearTimeout(settingsTimeout); }; @@ -892,7 +918,7 @@ const GraphViewer: React.FC = ({ onNodeRightClick={handleNodeRightClick} onNodeClick={handleNodeClick} onBackgroundClick={handleBackgroundClick} - linkCurvature={link => link.curve} + linkCurvature={link => link.curvature || 0} nodeCanvasObject={renderNode} onNodeDragEnd={(node => { node.fx = node.x; @@ -906,8 +932,8 @@ const GraphViewer: React.FC = ({ warmupTicks={forceSettings.warmupTicks.value} dagLevelDistance={forceSettings.dagLevelDistance.value} backgroundColor={backgroundColor} - onZoom={(zoom) => setCurrentZoom(zoom)} - onZoomEnd={(zoom) => setCurrentZoom(zoom)} + onZoom={handleZoom} + onZoomEnd={handleZoom} linkCanvasObject={renderLink} enableNodeDrag={!shouldUseSimpleRendering} autoPauseRedraw={true} @@ -917,7 +943,7 @@ const GraphViewer: React.FC = ({ {allowLasso && isLassoActive && } {minimap && graphData.nodes && - } diff --git a/flowsint-app/src/renderer/src/components/layout/info.tsx b/flowsint-app/src/renderer/src/components/layout/info.tsx index 4bc799c..5687d38 100644 --- a/flowsint-app/src/renderer/src/components/layout/info.tsx +++ b/flowsint-app/src/renderer/src/components/layout/info.tsx @@ -73,10 +73,7 @@ const InfoDialog = () => {

  • - Small to medium datasets (1-500 nodes): Interactive React Flow (reactflow.dev) graphs with real-time node dragging, detailed tooltips, and smooth animations for precise exploration. -
  • -
  • - Medium to large datasets (1-1500 nodes): Optimized React Force Graph (github.com/vasturiano/react-force-graph) layouts with force-directed algorithms that group related entities while maintaining interactive features for focused analysis. + Small to large datasets (1-1500 nodes): Optimized React Force Graph (github.com/vasturiano/react-force-graph) layouts with force-directed algorithms that group related entities while maintaining interactive features for focused analysis.
  • Large datasets (1550-100,000 nodes): High-performance Cosmograph (cosmograph.app) with advanced rendering techniques, allowing you to visualize complex networks without sacrificing responsiveness or browser stability. diff --git a/flowsint-app/src/renderer/src/lib/utils.ts b/flowsint-app/src/renderer/src/lib/utils.ts index f5c5644..29d73ad 100644 --- a/flowsint-app/src/renderer/src/lib/utils.ts +++ b/flowsint-app/src/renderer/src/lib/utils.ts @@ -1,122 +1,13 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" import Dagre from '@dagrejs/dagre'; - -import { type Edge, Position, type Node } from '@xyflow/react'; -import * as d3 from "d3-force" import { GraphEdge, GraphNode } from '@/types'; - -interface NodePosition { - x: number; - y: number; -} - -interface NodeMeasured { - width: number; - height: number; -} - -interface NodeInternals { - positionAbsolute: NodePosition; -} - -interface FlowNode { - measured: NodeMeasured; - internals: NodeInternals; -} - -interface IntersectionPoint { - x: number; - y: number; -} +import { FlowEdge, FlowNode } from "@/stores/flow-store"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -export const zoomSelector = (s: { transform: number[]; }) => s.transform[2] >= 0.6; - - -// this helper function returns the intersection point -// of the line between the center of the intersectionNode and the target node -function getNodeIntersection( - intersectionNode: FlowNode, - targetNode: FlowNode -): IntersectionPoint { - const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured; - const intersectionNodePosition = intersectionNode.internals.positionAbsolute; - const targetPosition = targetNode.internals.positionAbsolute; - - const w = intersectionNodeWidth / 2; - const h = intersectionNodeHeight / 2; - - const x2 = intersectionNodePosition.x + w; - const y2 = intersectionNodePosition.y + h; - const x1 = targetPosition.x + targetNode.measured.width / 2; - const y1 = targetPosition.y + targetNode.measured.height / 2; - - const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h); - const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h); - const a = 1 / (Math.abs(xx1) + Math.abs(yy1)); - const xx3 = a * xx1; - const yy3 = a * yy1; - const x = w * (xx3 + yy3) + x2; - const y = h * (-xx3 + yy3) + y2; - - return { x, y }; -} - -// returns the position (top,right,bottom or right) passed node compared to the intersection point -function getEdgePosition(node: FlowNode, intersectionPoint: IntersectionPoint): Position { - const n = { ...node.internals.positionAbsolute, ...node }; - const nx = Math.round(n.x); - const ny = Math.round(n.y); - const px = Math.round(intersectionPoint.x); - const py = Math.round(intersectionPoint.y); - - if (px <= nx + 1) { - return Position.Left; - } - if (px >= nx + n.measured.width - 1) { - return Position.Right; - } - if (py <= ny + 1) { - return Position.Top; - } - if (py >= n.y + n.measured.height - 1) { - return Position.Bottom; - } - - return Position.Top; -} - -// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge -interface EdgeParams { - sx: number; - sy: number; - tx: number; - ty: number; - sourcePos: Position; - targetPos: Position; -} - -export function getEdgeParams(source: FlowNode, target: FlowNode): EdgeParams { - const sourceIntersectionPoint = getNodeIntersection(source, target); - const targetIntersectionPoint = getNodeIntersection(target, source); - - const sourcePos = getEdgePosition(source, sourceIntersectionPoint); - const targetPos = getEdgePosition(target, targetIntersectionPoint); - - return { - sx: sourceIntersectionPoint.x, - sy: sourceIntersectionPoint.y, - tx: targetIntersectionPoint.x, - ty: targetIntersectionPoint.y, - sourcePos, - targetPos, - }; -} - interface LayoutOptions { direction?: "LR" | "TB"; strength?: number; @@ -124,81 +15,9 @@ interface LayoutOptions { iterations?: number; } -export const getForceLayoutedElements = ( - nodes: Node[], - edges: Edge[], - options: LayoutOptions = { - direction: "LR", - strength: -30, - distance: 10, - iterations: 300, - }, -) => { - // Create a map of node IDs to indices for the simulation - const nodeMap = new Map(nodes.map((node, i) => [node.id, i])) - - // Create a copy of nodes with positions for the simulation - const nodesCopy = nodes.map((node) => ({ - ...node, - x: node.position?.x || Math.random() * 500, - y: node.position?.y || Math.random() * 500, - width: node.measured?.width || 0, - height: node.measured?.height || 0, - })) - - // Create links for the simulation using indices - const links = edges.map((edge) => ({ - source: nodeMap.get(edge.source), - target: nodeMap.get(edge.target), - original: edge, - })) - - // Create the simulation - const simulation = d3 - .forceSimulation(nodesCopy) - .force( - "link", - d3.forceLink(links).id((d: any) => nodeMap.get(d.id)), - ) - .force("charge", d3.forceManyBody().strength(options.strength || -300)) - .force("center", d3.forceCenter(250, 250)) - .force( - "collision", - d3.forceCollide().radius((d: any) => Math.max(d.width, d.height) / 2 + 10), - ) - - // If direction is horizontal, adjust forces - if (options.direction === "LR") { - simulation.force("x", d3.forceX(250).strength(0.1)) - simulation.force("y", d3.forceY(250).strength(0.05)) - } else { - simulation.force("x", d3.forceX(250).strength(0.05)) - simulation.force("y", d3.forceY(250).strength(0.1)) - } - - // Run the simulation synchronously - simulation.stop() - for (let i = 0; i < (options.iterations || 300); i++) { - simulation.tick() - } - - // Update node positions based on simulation results - const updatedNodes = nodesCopy.map((node) => ({ - ...node, - position: { - x: node.x - node.width / 2, - y: node.y - node.height / 2, - }, - })) - - return { - nodes: updatedNodes, - edges, - } -} - -export const getDagreLayoutedElements = (nodes: GraphNode[] | any, - edges: GraphEdge[] | any, +// dagre layout function for the main graph component. +export const getDagreLayoutedElements = (nodes: GraphNode[], + edges: GraphEdge[], options: LayoutOptions = { direction: "TB", strength: -300, @@ -228,6 +47,38 @@ export const getDagreLayoutedElements = (nodes: GraphNode[] | any, }; }; +// dagre layout function for the flow component. +export const getFlowDagreLayoutedElements = (nodes: FlowNode[], + edges: FlowEdge[], + options: LayoutOptions = { + direction: "TB", + strength: -300, + distance: 10, + iterations: 300, + },) => { + + const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: options.direction }); + edges.forEach((edge) => g.setEdge(edge.source, edge.target)); + nodes.forEach((node) => + g.setNode(node.id, { + ...node, + width: node.measured?.width ?? 0, + height: node.measured?.height ?? 0, + }), + ); + Dagre.layout(g); + return { + nodes: nodes.map((node) => { + const position = g.node(node.id); + const x = position.x - (node.measured?.width ?? 0) / 2; + const y = position.y - (node.measured?.height ?? 0) / 2; + return { ...node, position: { x, y } }; + }), + edges, + }; +}; + export const sanitize = (name: string) => { return name @@ -364,7 +215,7 @@ export function deepObjectDiff(obj1: Dictionary, obj2: Dictionary): Dictionary { diffObject = { ...diffObject, [key]: { value, new: false, oldValue: obj1[key] ?? null, newValue: obj2[key] ?? null, identical: obj2[key] === obj1[key] } } } }) - // We map over the obj1 key:value duos to retrieve new keys that might have disapeared + // We map over the obj1 key:value duos to retrieve keys that might have disapeared Object.entries(obj1).forEach(([key, value]) => { // We check for additional keys if (!obj2.hasOwnProperty(key)) diff --git a/start.sh b/start.sh index 2de0189..db2d1dc 100755 --- a/start.sh +++ b/start.sh @@ -2,7 +2,7 @@ PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)" -echo "🚀 Starting Flowsint Project services..." +echo "Starting Flowsint project services..." cleanup() { echo "🛑 Stopping all services..."