From 880af5bbc4e4856abd8fe59e3e36bd6cc97e196d Mon Sep 17 00:00:00 2001 From: Sansar Kharal Date: Sun, 8 Mar 2026 20:52:04 -0600 Subject: [PATCH] Added problem map page and updated responsive app bar --- components/widgets/ResponsiveAppBar.js | 2 +- package-lock.json | 54 +- pages/problemmap/index.js | 820 +++++++++++++++++++++++++ 3 files changed, 866 insertions(+), 10 deletions(-) create mode 100644 pages/problemmap/index.js diff --git a/components/widgets/ResponsiveAppBar.js b/components/widgets/ResponsiveAppBar.js index 09ee18c..e0ddc1e 100644 --- a/components/widgets/ResponsiveAppBar.js +++ b/components/widgets/ResponsiveAppBar.js @@ -24,7 +24,7 @@ import MenuItem from '@mui/material/MenuItem'; import AdbIcon from '@mui/icons-material/Adb'; //const pages = ['Home', 'Tutorials', 'About Us', 'Navigation Graph']; -const pages = ['Home', 'About Us'] +const pages = ['Home', 'About Us', 'Problem Map'] const ResponsiveAppBar = (props) => { const [anchorElNav, setAnchorElNav] = React.useState(null); diff --git a/package-lock.json b/package-lock.json index 654b54c..81bb4b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.2.tgz", "integrity": "sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", @@ -735,6 +736,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -756,6 +758,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -816,6 +819,7 @@ "version": "11.9.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.9.0.tgz", "integrity": "sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.13.10", "@emotion/babel-plugin": "^11.7.1", @@ -859,6 +863,7 @@ "version": "11.8.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.8.1.tgz", "integrity": "sha512-OghEVAYBZMpEquHZwuelXcRjRJQOVayvbmNR0zr174NHdmMgrNkLC6TljKC5h9lZLkN5WGrdUcrKlOJ4phhoTQ==", + "peer": true, "dependencies": { "@babel/runtime": "^7.13.10", "@emotion/babel-plugin": "^11.7.1", @@ -1464,6 +1469,7 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.1.tgz", "integrity": "sha512-Vl3BHFzOcAT5TJfvzoQUyuo/Xckn+/NSRyJ8upM4Hbz6Y1egW6P8f1RCa4FdkEfPSd5wSSYdmPfAiEh8eI4rPg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.17.2", "@mui/base": "5.0.0-alpha.82", @@ -1906,6 +1912,7 @@ "version": "2.11.6", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2120,6 +2127,7 @@ "version": "18.0.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2279,6 +2287,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3211,6 +3220,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -3448,7 +3458,8 @@ "node_modules/d3-selection": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", - "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==" + "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", + "peer": true }, "node_modules/d3-shape": { "version": "3.1.0", @@ -3582,6 +3593,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -4039,6 +4051,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", "dev": true, + "peer": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", @@ -4199,6 +4212,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", @@ -6584,7 +6598,8 @@ "node_modules/memfs/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6693,6 +6708,7 @@ "version": "12.3.7", "resolved": "https://registry.npmjs.org/next/-/next-12.3.7.tgz", "integrity": "sha512-3PDn+u77s5WpbkUrslBP6SKLMeUj9cSx251LOt+yP9fgnqXV/ydny81xQsclz9R6RzCLONMCtwK2RvDdLa/mJQ==", + "peer": true, "dependencies": { "@next/env": "12.3.7", "@swc/helpers": "0.4.11", @@ -7319,6 +7335,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7359,6 +7376,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.22.0" @@ -8562,6 +8580,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.2.tgz", "integrity": "sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ==", + "peer": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", @@ -9000,12 +9019,14 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "peer": true, "requires": {} }, "@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==" + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "peer": true }, "@emotion/babel-plugin": { "version": "11.9.2", @@ -9060,6 +9081,7 @@ "version": "11.9.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.9.0.tgz", "integrity": "sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ==", + "peer": true, "requires": { "@babel/runtime": "^7.13.10", "@emotion/babel-plugin": "^11.7.1", @@ -9091,6 +9113,7 @@ "version": "11.8.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.8.1.tgz", "integrity": "sha512-OghEVAYBZMpEquHZwuelXcRjRJQOVayvbmNR0zr174NHdmMgrNkLC6TljKC5h9lZLkN5WGrdUcrKlOJ4phhoTQ==", + "peer": true, "requires": { "@babel/runtime": "^7.13.10", "@emotion/babel-plugin": "^11.7.1", @@ -9544,6 +9567,7 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.1.tgz", "integrity": "sha512-Vl3BHFzOcAT5TJfvzoQUyuo/Xckn+/NSRyJ8upM4Hbz6Y1egW6P8f1RCa4FdkEfPSd5wSSYdmPfAiEh8eI4rPg==", + "peer": true, "requires": { "@babel/runtime": "^7.17.2", "@mui/base": "5.0.0-alpha.82", @@ -9747,7 +9771,8 @@ "@popperjs/core": { "version": "2.11.6", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "peer": true }, "@prinsss/dvi2html": { "version": "0.0.1", @@ -9947,6 +9972,7 @@ "version": "18.0.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "peer": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -10057,7 +10083,8 @@ "version": "8.7.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -10739,7 +10766,8 @@ "d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true }, "d3-timer": { "version": "3.0.1", @@ -10800,7 +10828,8 @@ "d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true }, "d3-transition": { "version": "3.0.1", @@ -10977,7 +11006,8 @@ "d3-selection": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", - "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==" + "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", + "peer": true }, "d3-shape": { "version": "3.1.0", @@ -11333,6 +11363,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", "dev": true, + "peer": true, "requires": { "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", @@ -11464,6 +11495,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", "dev": true, + "peer": true, "requires": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", @@ -13213,7 +13245,8 @@ "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "peer": true } } }, @@ -13293,6 +13326,7 @@ "version": "12.3.7", "resolved": "https://registry.npmjs.org/next/-/next-12.3.7.tgz", "integrity": "sha512-3PDn+u77s5WpbkUrslBP6SKLMeUj9cSx251LOt+yP9fgnqXV/ydny81xQsclz9R6RzCLONMCtwK2RvDdLa/mJQ==", + "peer": true, "requires": { "@next/env": "12.3.7", "@next/swc-android-arm-eabi": "12.3.4", @@ -13728,6 +13762,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -13755,6 +13790,7 @@ "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.22.0" diff --git a/pages/problemmap/index.js b/pages/problemmap/index.js new file mode 100644 index 0000000..0312d09 --- /dev/null +++ b/pages/problemmap/index.js @@ -0,0 +1,820 @@ +// pages/problemmap/index.js + +import Head from "next/head"; +import { useEffect, useMemo, useRef, useState } from "react"; +import "bootstrap/dist/css/bootstrap.min.css"; + +import ResponsiveAppBar from "../../components/widgets/ResponsiveAppBar"; +import { requestProblems, requestReductionOptions } from "../../components/redux"; + +import { + Box, + Button, + Chip, + Container, + createTheme, + Divider, + Paper, + Stack, + Tab, + Tabs, + TextField, + ThemeProvider, + Typography, +} from "@mui/material"; + +export default function ProblemMapPage() { + const theme = createTheme({ + palette: { + mode: "light", + primary: { main: "#424242" }, + secondary: { main: "#f47920" }, + }, + }); + + // Backend base URL + const reduxBaseUrl = (process.env.NEXT_PUBLIC_REDUX_BASE_URL || "").trim(); + + // Ensure trailing slash + const safeApiUrl = useMemo(() => { + if (!reduxBaseUrl) return ""; + return reduxBaseUrl.endsWith("/") ? reduxBaseUrl : reduxBaseUrl + "/"; + }, [reduxBaseUrl]); + + /** + * Backend returns NPC_* names and does not expose true complexity metadata, + * so we classify in the frontend: + * - P_SET => P + * - everything else => NP-COMPLETE + */ + const P_SET = useMemo( + () => + new Set([ + "PRIMEFACTOR", + "DEUTSCH", + "DEUTSCHJOZSA", + "BERNSTEINVAZIRANI", + "SIMON", + ]), + [] + ); + + const canonicalKey = (name) => + String(name || "") + .trim() + .toUpperCase() + .replace(/^NPC_/, ""); + + const classifyProblem = (name) => { + const key = canonicalKey(name); + if (!key) return "UNKNOWN"; + if (P_SET.has(key)) return "P"; + return "NP-COMPLETE"; +}; + + const COMPLEXITY_TABS = [ + { label: "ALL", problemType: "ALL" }, + { label: "P", problemType: "P" }, + { label: "NP-COMPLETE", problemType: "NP-COMPLETE" }, + { label: "NP-HARD", problemType: "NP-HARD" }, + ]; + + const [tabIndex, setTabIndex] = useState(0); + const currentProblemType = COMPLEXITY_TABS[tabIndex]?.problemType ?? "ALL"; + + const [status, setStatus] = useState("Idle."); + const [loading, setLoading] = useState(false); + + const [problems, setProblems] = useState([]); // [{name, raw, class}] + const [edgesByProblem, setEdgesByProblem] = useState({}); // { [name]: string[] } + + const [query, setQuery] = useState(""); + const [selected, setSelected] = useState(null); + const [showOnlySelectedEdges, setShowOnlySelectedEdges] = useState(false); + + // For Inspector explanation: selected -> focusTarget + const [focusTarget, setFocusTarget] = useState(null); + + // ---- helpers ------------------------------------------------------------ + const normalizeProblems = (raw) => { + if (!Array.isArray(raw)) return []; + + return raw + .map((p) => { + let name = + p?.problemName ?? + p?.name ?? + p?.id ?? + p?.problem ?? + (typeof p === "string" ? p : null); + + // If it's a JSON string like: "{ \"problemName\": \"NPC_CLIQUE\" }" + if (typeof name === "string" && name.trim().startsWith("{")) { + try { + const parsed = JSON.parse(name); + name = parsed?.problemName ?? parsed?.name ?? name; + } catch { + // ignore + } + } + + if (!name) return null; + const n = String(name).trim(); + if (!n) return null; + + return { name: n, raw: p, class: classifyProblem(n) }; + }) + .filter(Boolean) + .sort((a, b) => a.name.localeCompare(b.name)); + }; + + // Remove prefix for display only + const stripPrefix = (name) => String(name || "").replace(/^NPC_/, ""); + + const runWithConcurrency = async (items, limit, worker) => { + const results = new Array(items.length); + let i = 0; + + const runners = new Array(Math.min(limit, items.length)) + .fill(null) + .map(async () => { + while (true) { + const idx = i++; + if (idx >= items.length) break; + results[idx] = await worker(items[idx], idx); + } + }); + + await Promise.all(runners); + return results; + }; + + const explainReduction = (from, to, viewType) => { + if (!from || !to) return ""; + + const fromClass = classifyProblem(from); + const toClass = classifyProblem(to); + + return ( + `Reduction: ${stripPrefix(from)} → ${stripPrefix(to)}\n\n` + + `Classes (frontend view):\n` + + `• ${stripPrefix(from)}: ${fromClass}\n` + + `• ${stripPrefix(to)}: ${toClass}\n\n` + + `Meaning:\n` + + `There is an efficient (polynomial-time) transformation that converts any instance of "${stripPrefix( + from + )}" into an instance of "${stripPrefix(to)}".\n\n` + + `Why it matters:\n` + + `If we can solve "${stripPrefix(to)}" efficiently, then we can solve "${stripPrefix(from)}" efficiently by converting it into "${stripPrefix(to)}" first.\n\n` + + `Notation:\n` + + `"${stripPrefix(from)} ≤p ${stripPrefix(to)}"\n\n` + + `View: ${viewType}` + ); + }; + + // ---- data fetch --------------------------------------------------------- + const loadProblems = async () => { + if (!safeApiUrl) { + setStatus( + "NEXT_PUBLIC_REDUX_BASE_URL is not set. Problem Map cannot load from backend." + ); + return; + } + try { + setStatus("Loading problems..."); + const rawProblems = await requestProblems(safeApiUrl); + const normalized = normalizeProblems(rawProblems); + setProblems(normalized); + setStatus(`Loaded ${normalized.length} problems.`); + } catch (err) { + console.error(err); + setStatus("Failed to load problems from backend."); + } + }; + + const loadEdgesForType = async () => { + if (!safeApiUrl) return; + + setLoading(true); + setSelected(null); + setFocusTarget(null); + setShowOnlySelectedEdges(false); + + try { + // Pick visible nodes based on classification + const nodesForThisTab = + currentProblemType === "ALL" + ? problems.map((p) => p.name) + : currentProblemType === "P" + ? problems.filter((p) => p.class === "P").map((p) => p.name) + : currentProblemType === "NP-COMPLETE" + ? problems.filter((p) => p.class === "NP-COMPLETE").map((p) => p.name) + : // NP-HARD tab for now = "everything not P" + problems.filter((p) => p.class !== "P").map((p) => p.name); + + setStatus( + `Loading reductions for ${currentProblemType} (${nodesForThisTab.length} nodes visible)...` + ); + + if (!nodesForThisTab.length) { + setEdgesByProblem({}); + setStatus(`No problems found for ${currentProblemType}.`); + return; + } + + // ✅ SKIP reductions for P (you wanted list-only + no edges) + if (currentProblemType === "P") { + const emptyMap = {}; + nodesForThisTab.forEach((n) => (emptyMap[n] = [])); + setEdgesByProblem(emptyMap); + setStatus( + `Loaded ${nodesForThisTab.length} P problems (list view; no reductions).` + ); + return; + } + + const visibleSet = new Set(nodesForThisTab); + + const edgePairs = await runWithConcurrency( + nodesForThisTab, + 6, + async (problemName) => { + try { + // Always ask backend for reductions in NPC graph world + const opts = await requestReductionOptions( + safeApiUrl, + problemName, + "NPC" + ); + + const targets = Array.isArray(opts) + ? opts + .map( + (x) => + x?.problemName ?? + x?.name ?? + x?.id ?? + (typeof x === "string" ? x : null) + ) + .filter(Boolean) + .map(String) + : []; + + // keep only edges inside current tab + const cleaned = Array.from(new Set(targets)).filter((t) => + visibleSet.has(t) + ); + + return [problemName, cleaned]; + } catch (e) { + return [problemName, []]; + } + } + ); + + const map = {}; + for (const [from, tos] of edgePairs) map[from] = tos; + + setEdgesByProblem(map); + setStatus(`Loaded graph: ${nodesForThisTab.length} nodes.`); + } catch (err) { + console.error(err); + setStatus("Failed to load reductions for this view."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!safeApiUrl) return; + loadProblems(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [safeApiUrl]); + + useEffect(() => { + if (!safeApiUrl) return; + if (!problems.length) return; + loadEdgesForType(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [safeApiUrl, currentProblemType, problems.length]); + + // ---- filtering (TYPE + search) ----------------------------------------- + const filteredProblems = useMemo(() => { + const q = query.trim().toLowerCase(); + + let base = + currentProblemType === "ALL" + ? problems + : currentProblemType === "P" + ? problems.filter((p) => p.class === "P") + : currentProblemType === "NP-COMPLETE" + ? problems.filter((p) => p.class === "NP-COMPLETE") + : problems.filter((p) => p.class !== "P"); // NP-HARD for now + + if (!q) return base; + return base.filter((p) => p.name.toLowerCase().includes(q)); + }, [problems, query, currentProblemType]); + + const nodeNames = useMemo( + () => filteredProblems.map((p) => p.name), + [filteredProblems] + ); + + const visibleEdgeList = useMemo(() => { + const set = new Set(nodeNames); + const list = []; + + for (const from of nodeNames) { + const tos = edgesByProblem[from] || []; + for (const to of tos) { + if (!set.has(to)) continue; + + if (showOnlySelectedEdges) { + if (!selected) continue; + if (from !== selected && to !== selected) continue; + } + + list.push({ from, to }); + } + } + + return list; + }, [nodeNames, edgesByProblem, showOnlySelectedEdges, selected]); + + // ---- layout / SVG ------------------------------------------------------- + const svgRef = useRef(null); + + const layout = useMemo(() => { + const W = 980; + const H = 600; + const cx = W / 2; + const cy = H / 2; + const r = Math.min(W, H) * 0.36; + + const pos = {}; + const n = nodeNames.length; + + for (let i = 0; i < n; i++) { + const angle = (2 * Math.PI * i) / Math.max(1, n) - Math.PI / 2; + pos[nodeNames[i]] = { + x: cx + r * Math.cos(angle), + y: cy + r * Math.sin(angle), + }; + } + + return { W, H, pos }; + }, [nodeNames]); + + const getNodeRadius = (name) => (name === selected ? 12 : 9); + + const edgePath = (from, to) => { + const a = layout.pos[from]; + const b = layout.pos[to]; + if (!a || !b) return ""; + + const dx = b.x - a.x; + const dy = b.y - a.y; + const len = Math.max(1, Math.hypot(dx, dy)); + + const mx = (a.x + b.x) / 2; + const my = (a.y + b.y) / 2; + const ox = (-dy / len) * 22; + const oy = (dx / len) * 22; + + const cx = mx + ox; + const cy = my + oy; + + const ra = getNodeRadius(from) + 2; + const rb = getNodeRadius(to) + 6; + + const sx = a.x + (dx / len) * ra; + const sy = a.y + (dy / len) * ra; + const ex = b.x - (dx / len) * rb; + const ey = b.y - (dy / len) * rb; + + return `M ${sx} ${sy} Q ${cx} ${cy} ${ex} ${ey}`; + }; + + const selectedOutgoing = useMemo(() => { + if (!selected) return []; + return edgesByProblem[selected] || []; + }, [selected, edgesByProblem]); + + const selectedIncoming = useMemo(() => { + if (!selected) return []; + const inc = []; + for (const [from, tos] of Object.entries(edgesByProblem)) { + if (tos?.includes(selected)) inc.push(from); + } + return inc.sort((a, b) => a.localeCompare(b)); + }, [selected, edgesByProblem]); + + // ---- UI ---------------------------------------------------------------- + return ( + + + Redux | Problem Map + + + + + + + + Problem Map + + + + Status:{" "} + {safeApiUrl + ? status + : "NEXT_PUBLIC_REDUX_BASE_URL is not set, so I can’t load problems from the backend."} + + + setTabIndex(v)} + variant="scrollable" + scrollButtons="auto" + sx={{ mb: 2 }} + > + {COMPLEXITY_TABS.map((t) => ( + + ))} + + + + setQuery(e.target.value)} + sx={{ minWidth: 260 }} + /> + + + + + + {selected ? ( + { + setSelected(null); + setFocusTarget(null); + }} + /> + ) : ( + + )} + + + + + + {/* Graph / List */} + + + {/* ✅ LIST VIEW FOR P */} + {currentProblemType === "P" ? ( + + + P Problems (List) + + + {nodeNames.length === 0 ? ( + + No P problems found. + + ) : ( + + {nodeNames.map((name) => ( + { + setSelected(name); + setFocusTarget(null); + }} + /> + ))} + + )} + + ) : ( + + + + + + + + {/* edges */} + {visibleEdgeList.map((e, idx) => { + const focused = + selected && + focusTarget && + e.from === selected && + e.to === focusTarget; + const hot = + focused || + (selected && + (e.from === selected || e.to === selected)); + + return ( + ${e.to}-${idx}`} + d={edgePath(e.from, e.to)} + fill="none" + stroke={ + focused + ? "rgba(244,121,32,1)" + : hot + ? "rgba(244,121,32,0.9)" + : "rgba(0,0,0,0.22)" + } + strokeWidth={focused ? 3.2 : hot ? 2.2 : 1.2} + markerEnd="url(#arrow)" + /> + ); + })} + + {/* nodes */} + {nodeNames.map((name) => { + const p = layout.pos[name]; + if (!p) return null; + const isSelected = name === selected; + const c = classifyProblem(name); + + const fill = + c === "P" + ? "rgba(120,120,120,0.85)" + : "rgba(66,66,66,0.85)"; + + return ( + { + setSelected(name); + setFocusTarget(null); + }} + > + + + {stripPrefix(name).length > 18 + ? stripPrefix(name).slice(0, 18) + "…" + : stripPrefix(name)} + + + ); + })} + + )} + + + Showing {nodeNames.length} nodes and{" "} + {currentProblemType === "P" ? 0 : visibleEdgeList.length}{" "} + directed edges for {currentProblemType}. + + + + + {/* Inspector */} + + + + Inspector + + + {!selected ? ( + + Click a node to see its incoming/outgoing reductions and an + explanation. + + ) : ( + <> + + Problem: {stripPrefix(selected)}{" "} + + ({classifyProblem(selected)}) + + + + + + + Outgoing (to): + + + {selectedOutgoing.length ? ( + + {selectedOutgoing + .filter((t) => nodeNames.includes(t)) + .sort((a, b) => a.localeCompare(b)) + .slice(0, 60) + .map((t) => ( + setFocusTarget(t)} + /> + ))} + + ) : ( + + None found. + + )} + + + + + Incoming (from): + + + {selectedIncoming.length ? ( + + {selectedIncoming + .filter((t) => nodeNames.includes(t)) + .slice(0, 60) + .map((t) => ( + { + setSelected(t); + setFocusTarget(null); + }} + /> + ))} + + ) : ( + + None visible. + + )} + + + + + Reduction Explanation + + + + {focusTarget + ? explainReduction(selected, focusTarget, currentProblemType) + : `Click an outgoing target chip above to explain a specific reduction.\n\nGeneral idea:\nA → B means we can transform A into B efficiently. So if B is easy to solve, then A becomes easy too.`} + + + {focusTarget && ( + + )} + + + + + + )} + + + + + {!safeApiUrl && ( + + + + Backend not configured + + + Set: +
+ NEXT_PUBLIC_REDUX_BASE_URL=http://localhost:27000/ +
+ Then restart: +
+ Ctrl+C and npm run dev +
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file