diff --git a/package-lock.json b/package-lock.json index 6dd9a082..128d07d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.15", "@mui/x-data-grid": "^6.0.4", - "@qdrant/js-client-rest": "^1.8.1", + "@qdrant/js-client-rest": "^1.10.0", "@saehrimnir/druidjs": "^0.6.3", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -34,6 +34,7 @@ "axios": "^1.6.7", "chart.js": "^4.3.0", "chroma-js": "^2.4.2", + "force-graph": "^1.43.5", "jose": "^5.2.3", "jsonc-parser": "^3.2.0", "lodash": "^4.17.21", @@ -1684,9 +1685,9 @@ } }, "node_modules/@qdrant/js-client-rest": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.8.2.tgz", - "integrity": "sha512-BCGC4YRcqjRxXVo500CxjhluPpGO0XpOwojauT8675Duv24YTlkhvDRmc1c9k/df2+yH/typtkecK3VOi3CD7A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.10.0.tgz", + "integrity": "sha512-oAONh3z9yNxxeM5BLAe5oXV7Fx3GoqHW68gNN8oxDikQvO3SIywQXN633kU0X/zrcx3tU+OA6NuUniU5GANaRg==", "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "@sevinf/maybe": "0.5.0", @@ -1889,6 +1890,11 @@ "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==" }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.2", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==" + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -2612,6 +2618,14 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "node_modules/accessor-fn": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.0.tgz", + "integrity": "sha512-dml7D96DY/K5lt4Ra2jMnpL9Bhw5HEGws4p1OAIxFFj9Utd/RxNfEO3T3f0QIWFNwQU7gNxH9snUfqF/zNkP/w==", + "engines": { + "node": ">=12" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -2846,6 +2860,15 @@ "integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==", "license": "MIT" }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -2967,6 +2990,17 @@ } ] }, + "node_modules/canvas-color-tracker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.2.1.tgz", + "integrity": "sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3266,6 +3300,203 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz", + "integrity": "sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.0.2.tgz", + "integrity": "sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==" + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/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==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", @@ -4147,6 +4378,30 @@ "is-callable": "^1.1.3" } }, + "node_modules/force-graph": { + "version": "1.43.5", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.43.5.tgz", + "integrity": "sha512-HveLELh9yhZXO/QOfaFS38vlwJZ/3sKu+jarfXzRmbmihSOH/BbRWnUvmg8wLFiYy6h4HlH4lkRfZRccHYmXgA==", + "dependencies": { + "@tweenjs/tween.js": "18 - 23", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "1", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "index-array-by": "1", + "kapsule": "^1.14", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4578,6 +4833,14 @@ "node": ">=8" } }, + "node_modules/index-array-by": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.1.tgz", + "integrity": "sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==", + "engines": { + "node": ">=12" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4615,6 +4878,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -5159,6 +5430,17 @@ "node": ">=4.0" } }, + "node_modules/kapsule": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.14.5.tgz", + "integrity": "sha512-H0iSpTynUzZw3tgraDmReprpFRmH5oP5GPmaNsurSwLx2H5iCpOMIkp5q+sfhB4Tz/UJd1E1IbEE9Z6ksnJ6RA==", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -5243,6 +5525,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7609,6 +7896,11 @@ "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/tinypool": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", @@ -9695,9 +9987,9 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@qdrant/js-client-rest": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.8.2.tgz", - "integrity": "sha512-BCGC4YRcqjRxXVo500CxjhluPpGO0XpOwojauT8675Duv24YTlkhvDRmc1c9k/df2+yH/typtkecK3VOi3CD7A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.10.0.tgz", + "integrity": "sha512-oAONh3z9yNxxeM5BLAe5oXV7Fx3GoqHW68gNN8oxDikQvO3SIywQXN633kU0X/zrcx3tU+OA6NuUniU5GANaRg==", "requires": { "@qdrant/openapi-typescript-fetch": "1.2.6", "@sevinf/maybe": "0.5.0", @@ -9828,6 +10120,11 @@ "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==" }, + "@tweenjs/tween.js": { + "version": "23.1.2", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==" + }, "@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -10393,6 +10690,11 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "accessor-fn": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.0.tgz", + "integrity": "sha512-dml7D96DY/K5lt4Ra2jMnpL9Bhw5HEGws4p1OAIxFFj9Utd/RxNfEO3T3f0QIWFNwQU7gNxH9snUfqF/zNkP/w==" + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -10557,6 +10859,11 @@ "resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz", "integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==" }, + "bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==" + }, "blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -10622,6 +10929,14 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001578.tgz", "integrity": "sha512-J/jkFgsQ3NEl4w2lCoM9ZPxrD+FoBNJ7uJUpGVjIg/j0OwJosWM36EPDv+Yyi0V4twBk9pPmlFS+PLykgEvUmg==" }, + "canvas-color-tracker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.2.1.tgz", + "integrity": "sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==", + "requires": { + "tinycolor2": "^1.6.0" + } + }, "ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -10834,6 +11149,149 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==" + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-force-3d": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz", + "integrity": "sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==", + "requires": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-octree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.0.2.tgz", + "integrity": "sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "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==" + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, "data-urls": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", @@ -11482,6 +11940,27 @@ "is-callable": "^1.1.3" } }, + "force-graph": { + "version": "1.43.5", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.43.5.tgz", + "integrity": "sha512-HveLELh9yhZXO/QOfaFS38vlwJZ/3sKu+jarfXzRmbmihSOH/BbRWnUvmg8wLFiYy6h4HlH4lkRfZRccHYmXgA==", + "requires": { + "@tweenjs/tween.js": "18 - 23", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "1", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "index-array-by": "1", + "kapsule": "^1.14", + "lodash-es": "4" + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -11769,6 +12248,11 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, + "index-array-by": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.1.tgz", + "integrity": "sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -11800,6 +12284,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -12141,6 +12630,14 @@ "object.assign": "^4.1.3" } }, + "kapsule": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.14.5.tgz", + "integrity": "sha512-H0iSpTynUzZw3tgraDmReprpFRmH5oP5GPmaNsurSwLx2H5iCpOMIkp5q+sfhB4Tz/UJd1E1IbEE9Z6ksnJ6RA==", + "requires": { + "lodash-es": "4" + } + }, "kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -12198,6 +12695,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -13748,6 +14250,11 @@ "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", "dev": true }, + "tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "tinypool": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", diff --git a/package.json b/package.json index ddc4a1f6..d514706b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.15", "@mui/x-data-grid": "^6.0.4", - "@qdrant/js-client-rest": "^1.8.1", + "@qdrant/js-client-rest": "^1.10.0", "@saehrimnir/druidjs": "^0.6.3", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -29,6 +29,7 @@ "axios": "^1.6.7", "chart.js": "^4.3.0", "chroma-js": "^2.4.2", + "force-graph": "^1.43.5", "jose": "^5.2.3", "jsonc-parser": "^3.2.0", "lodash": "^4.17.21", diff --git a/src/components/FilterEditorWindow/config/Autocomplete.js b/src/components/FilterEditorWindow/config/Autocomplete.js index 9457451f..519d5950 100644 --- a/src/components/FilterEditorWindow/config/Autocomplete.js +++ b/src/components/FilterEditorWindow/config/Autocomplete.js @@ -1,6 +1,6 @@ import { OpenapiAutocomplete } from 'autocomplete-openapi/src/autocomplete'; -export const autocomplete = async (monaco, qdrantClient, collectionName) => { +export const autocomplete = async (monaco, qdrantClient, collectionName, customRequestSchema) => { const response = await fetch(import.meta.env.BASE_URL + './openapi.json'); const openapi = await response.json(); @@ -15,41 +15,8 @@ export const autocomplete = async (monaco, qdrantClient, collectionName) => { } catch (e) { console.error(e); } - const FilterRequest = { - description: 'Filter request', - type: 'object', - properties: { - limit: { - description: 'Page size. Default: 10', - type: 'integer', - format: 'uint', - minimum: 1, - nullable: true, - }, - filter: { - description: 'Look only for points which satisfies this conditions. If not provided - all points.', - anyOf: [ - { - $ref: '#/components/schemas/Filter', - }, - { - nullable: true, - }, - ], - }, - vector_name: { - description: 'Vector field name', - type: 'string', - enum: vectorNames, - }, - color_by: { - description: 'Color points by this field', - type: 'string', - nullable: true, - }, - }, - }; - openapi.components.schemas.FilterRequest = FilterRequest; + + openapi.components.schemas.CustomRequest = customRequestSchema(vectorNames); const autocomplete = new OpenapiAutocomplete(openapi, []); @@ -79,7 +46,7 @@ export const autocomplete = async (monaco, qdrantClient, collectionName) => { const requestBody = requestBodyLines.join('\n'); - let suggestions = autocomplete.completeRequestBodyByDataRef('#/components/schemas/FilterRequest', requestBody); + let suggestions = autocomplete.completeRequestBodyByDataRef('#/components/schemas/CustomRequest', requestBody); suggestions = suggestions.map((s) => { return { label: s, diff --git a/src/components/FilterEditorWindow/config/RequestFromCode.js b/src/components/FilterEditorWindow/config/RequestFromCode.js index 6e9f3c3a..27e9af7a 100644 --- a/src/components/FilterEditorWindow/config/RequestFromCode.js +++ b/src/components/FilterEditorWindow/config/RequestFromCode.js @@ -1,11 +1,54 @@ import axios from 'axios'; import { bigIntJSON } from '../../../common/bigIntJSON'; -export async function requestFromCode(text, collectionName) { - const data = codeParse(text); - if (data.error) { - return data; +function parseDataToRequest(reqBody) { + // Validate color_by + if (reqBody.color_by) { + const colorBy = reqBody.color_by; + + if (typeof colorBy === 'string') { + // Parse into payload variant + reqBody.color_by = { + payload: colorBy, + }; + } else { + // Check we only have one of the options: payload, or discover_score + const options = [colorBy.payload, colorBy.discover_score]; + const optionsCount = options.filter((option) => option).length; + if (optionsCount !== 1) { + return { + reqBody: reqBody, + error: '`color_by`: Only one of `payload`, or `discover_score` can be used', + }; + } + + // Put search arguments in main request body + if (colorBy.discover_score) { + reqBody = { + ...reqBody, + ...colorBy.discover_score, + }; + } + } + } + + // Set with_vector name + if (reqBody.vector_name) { + reqBody.with_vector = [reqBody.vector_name]; + return { + reqBody: reqBody, + error: null, + }; + } else if (!reqBody.vector_name) { + reqBody.with_vector = true; + return { + reqBody: reqBody, + error: null, + }; } +} +export async function requestFromCode(dataRaw, collectionName) { + const data = parseDataToRequest(dataRaw); // Sending request const colorBy = data.reqBody.color_by; if (colorBy?.payload) { @@ -108,12 +151,10 @@ async function discoverFromCode(collectionName, data) { } export function codeParse(codeText) { - let reqBody = {}; - // Parse JSON if (codeText) { try { - reqBody = bigIntJSON.parse(codeText); + return bigIntJSON.parse(codeText); } catch (e) { return { reqBody: codeText, @@ -121,49 +162,4 @@ export function codeParse(codeText) { }; } } - - // Validate color_by - if (reqBody.color_by) { - const colorBy = reqBody.color_by; - - if (typeof colorBy === 'string') { - // Parse into payload variant - reqBody.color_by = { - payload: colorBy, - }; - } else { - // Check we only have one of the options: payload, or discover_score - const options = [colorBy.payload, colorBy.discover_score]; - const optionsCount = options.filter((option) => option).length; - if (optionsCount !== 1) { - return { - reqBody: reqBody, - error: '`color_by`: Only one of `payload`, or `discover_score` can be used', - }; - } - - // Put search arguments in main request body - if (colorBy.discover_score) { - reqBody = { - ...reqBody, - ...colorBy.discover_score, - }; - } - } - } - - // Set with_vector name - if (reqBody.vector_name) { - reqBody.with_vector = [reqBody.vector_name]; - return { - reqBody: reqBody, - error: null, - }; - } else if (!reqBody.vector_name) { - reqBody.with_vector = true; - return { - reqBody: reqBody, - error: null, - }; - } } diff --git a/src/components/FilterEditorWindow/index.jsx b/src/components/FilterEditorWindow/index.jsx index f8fed576..0250fe4f 100644 --- a/src/components/FilterEditorWindow/index.jsx +++ b/src/components/FilterEditorWindow/index.jsx @@ -5,16 +5,17 @@ import { useParams } from 'react-router-dom'; import { useClient } from '../../context/client-context'; import { useTheme } from '@mui/material/styles'; import { autocomplete } from './config/Autocomplete'; -import { requestFromCode } from './config/RequestFromCode'; +import { useSnackbar } from 'notistack'; +import { codeParse } from './config/RequestFromCode'; import './editor.css'; import EditorCommon from '../EditorCommon'; -const CodeEditorWindow = ({ onChange, code, onChangeResult }) => { +const CodeEditorWindow = ({ onChange, code, onChangeResult, customRequestSchema }) => { + const { enqueueSnackbar } = useSnackbar(); const editorRef = useRef(null); const lensesRef = useRef(null); const autocompleteRef = useRef(null); const { collectionName } = useParams(); - const { client: qdrantClient } = useClient(); let runBtnCommandId = null; @@ -29,6 +30,17 @@ const CodeEditorWindow = ({ onChange, code, onChangeResult }) => { [] ); + function onRun(codeText) { + const data = codeParse(codeText); + if (data.error) { + enqueueSnackbar(`Visualization Unsuccessful, error: ${JSON.stringify(data.error)}`, { + variant: 'error', + }); + return data; + } + onChangeResult(data, collectionName); + } + function handleEditorDidMount(editor, monaco) { editorRef.current = editor; let decorations = []; @@ -36,9 +48,7 @@ const CodeEditorWindow = ({ onChange, code, onChangeResult }) => { runBtnCommandId = editor.addCommand( 0, async (_ctx, ...args) => { - const data = args[0]; - const result = await requestFromCode(data, collectionName); - onChangeResult(result); + onRun(args[0]); }, '' ); @@ -74,14 +84,13 @@ const CodeEditorWindow = ({ onChange, code, onChangeResult }) => { ); editor.addCommand(monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter, async () => { const data = selectedCodeBlock.blockText; - const result = await requestFromCode(data, collectionName); - onChangeResult(result); + onRun(data); }); } }); } function handleEditorWillMount(monaco) { - autocomplete(monaco, qdrantClient, collectionName).then((autocomplete) => { + autocomplete(monaco, qdrantClient, collectionName, customRequestSchema).then((autocomplete) => { autocompleteRef.current = monaco.languages.registerCompletionItemProvider('custom-language', autocomplete); }); } @@ -107,5 +116,6 @@ CodeEditorWindow.propTypes = { onChange: PropTypes.func.isRequired, code: PropTypes.string.isRequired, onChangeResult: PropTypes.func.isRequired, + customRequestSchema: PropTypes.func.isRequired, }; export default CodeEditorWindow; diff --git a/src/components/GraphVisualisation/GraphVisualisation.jsx b/src/components/GraphVisualisation/GraphVisualisation.jsx new file mode 100644 index 00000000..4d6d0acc --- /dev/null +++ b/src/components/GraphVisualisation/GraphVisualisation.jsx @@ -0,0 +1,100 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { deduplicatePoints, getSimilarPoints, initGraph } from '../../lib/graph-visualization-helpers'; +import ForceGraph from 'force-graph'; +import { useClient } from '../../context/client-context'; +import { useSnackbar } from 'notistack'; + +const GraphVisualisation = ({ initNode, options, onDataDisplay, wrapperRef }) => { + const graphRef = useRef(null); + const { client: qdrantClient } = useClient(); + const { enqueueSnackbar } = useSnackbar(); + const NODE_R = 4; + let highlightedNode = null; + + const handleNodeClick = async (node) => { + node.clicked = true; + const { nodes, links } = graphRef.current.graphData(); + const pointId = node.id; + + let similarPoints = []; + try { + similarPoints = await getSimilarPoints(qdrantClient, { + collectionName: options.collectionName, + pointId, + limit: options.limit, + filter: options.filter, + using: options.using, + }); + } catch (e) { + enqueueSnackbar(e.message, { variant: 'error' }); + return; + } + + graphRef.current.graphData({ + nodes: [...nodes, ...deduplicatePoints(nodes, similarPoints)], + links: [...links, ...similarPoints.map((point) => ({ source: pointId, target: point.id }))], + }); + }; + + useEffect(() => { + const elem = document.getElementById('graph'); + // eslint-disable-next-line new-cap + graphRef.current = ForceGraph()(elem) + .nodeColor((node) => (node.clicked ? '#e94' : '#2cb')) + .onNodeHover((node) => { + if (!node) { + elem.style.cursor = 'default'; + return; + } + node.aa = 1; + elem.style.cursor = 'pointer'; + highlightedNode = node; + onDataDisplay(node); + }) + .autoPauseRedraw(false) + .nodeCanvasObjectMode((node) => (node?.id === highlightedNode?.id ? 'before' : undefined)) + .nodeCanvasObject((node, ctx) => { + if (!node) return; + // add ring for last hovered nodes + ctx.beginPath(); + ctx.arc(node.x, node.y, NODE_R * 1.4, 0, 2 * Math.PI, false); + ctx.fillStyle = node.id === highlightedNode?.id ? '#817' : 'transparent'; + ctx.fill(); + }) + .linkColor(() => '#a6a6a6'); + }, [initNode, options]); + + useEffect(() => { + graphRef.current.width(wrapperRef?.clientWidth).height(wrapperRef?.clientHeight); + }, [wrapperRef, initNode, options]); + + useEffect(() => { + const initNewGraph = async () => { + const graphData = await initGraph(qdrantClient, { + ...options, + initNode, + }); + if (graphRef.current && options) { + const initialActiveNode = graphData.nodes[0]; + onDataDisplay(initialActiveNode); + highlightedNode = initialActiveNode; + graphRef.current.graphData(graphData).linkDirectionalArrowLength(3).onNodeClick(handleNodeClick); + } + }; + initNewGraph().catch((e) => { + enqueueSnackbar(JSON.stringify(e.getActualType()), { variant: 'error' }); + }); + }, [initNode, options]); + + return
; +}; + +GraphVisualisation.propTypes = { + initNode: PropTypes.object, + options: PropTypes.object.isRequired, + onDataDisplay: PropTypes.func.isRequired, + wrapperRef: PropTypes.object, +}; + +export default GraphVisualisation; diff --git a/src/components/GraphVisualisation/PointPreview.jsx b/src/components/GraphVisualisation/PointPreview.jsx new file mode 100644 index 00000000..0313025d --- /dev/null +++ b/src/components/GraphVisualisation/PointPreview.jsx @@ -0,0 +1,84 @@ +import React, { memo } from 'react'; +import PropTypes from 'prop-types'; +import { useTheme } from '@mui/material/styles'; +import { alpha, Box, Card, CardContent, CardHeader, Grid, LinearProgress } from '@mui/material'; +import { DataGridList } from '../Points/DataGridList'; +import PointImage from '../Points/PointImage'; +import Vectors from '../Points/PointVectors'; + +const PointPreview = ({ point }) => { + const theme = useTheme(); + const [loading] = React.useState(false); + const conditions = []; + const payloadSchema = {}; + const onConditionChange = () => {}; + + if (!point) { + return null; + } + + return ( + <> + + {loading && ( + + + + )} + {Object.keys(point.payload).length > 0 && ( + <> + + + {point.payload && } + + + + + + + )} + {point?.vector && ( + <> + + + + + + )} + + + ); +}; + +// prop types +PointPreview.propTypes = { + point: PropTypes.object, +}; + +export default memo(PointPreview); diff --git a/src/components/Points/PointCard.jsx b/src/components/Points/PointCard.jsx index ad08c2a1..ff6fb9c5 100644 --- a/src/components/Points/PointCard.jsx +++ b/src/components/Points/PointCard.jsx @@ -125,7 +125,9 @@ const PointCard = (props) => { payloadSchema={props.payloadSchema} /> - {point.payload && } + + {point.payload && } + diff --git a/src/components/Points/PointImage.jsx b/src/components/Points/PointImage.jsx index e26a7566..d58492bc 100644 --- a/src/components/Points/PointImage.jsx +++ b/src/components/Points/PointImage.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Box, CardMedia, Grid, Modal, Typography } from '@mui/material'; +import { Box, CardMedia, Modal, Typography } from '@mui/material'; function PointImage({ data, sx }) { const [fullScreenImg, setFullScreenImg] = useState(null); @@ -58,7 +58,7 @@ function PointImage({ data, sx }) { } return ( - + <> {images} - + ); } PointImage.propTypes = { diff --git a/src/components/VisualizeChart/index.jsx b/src/components/VisualizeChart/index.jsx index 5f0d2c7a..4b55901f 100644 --- a/src/components/VisualizeChart/index.jsx +++ b/src/components/VisualizeChart/index.jsx @@ -1,4 +1,3 @@ -import { Button } from '@mui/material'; import Chart from 'chart.js/auto'; import chroma from 'chroma-js'; import get from 'lodash/get'; @@ -12,20 +11,9 @@ import { bigIntJSON } from '../../common/bigIntJSON'; const SCORE_GRADIENT_COLORS = ['#EB5353', '#F9D923', '#36AE7C']; const VisualizeChart = ({ scrollResult }) => { - const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const { enqueueSnackbar } = useSnackbar(); const [openViewPoints, setOpenViewPoints] = useState(false); const [viewPoints, setViewPoint] = useState([]); - const action = (snackbarId) => ( - - ); useEffect(() => { if (!scrollResult.data && !scrollResult.error) { @@ -35,14 +23,12 @@ const VisualizeChart = ({ scrollResult }) => { if (scrollResult.error) { enqueueSnackbar(`Visualization Unsuccessful, error: ${bigIntJSON.stringify(scrollResult.error)}`, { variant: 'error', - action, }); return; } else if (!scrollResult.data?.result?.points.length) { enqueueSnackbar(`Visualization Unsuccessful, error: No data returned`, { variant: 'error', - action, }); return; } @@ -57,7 +43,6 @@ const VisualizeChart = ({ scrollResult }) => { if (get(scrollResult.data.result?.points[0]?.payload, labelby) === undefined) { enqueueSnackbar(`Visualization Unsuccessful, error: Color by field ${labelby} does not exist`, { variant: 'error', - action, }); return; } @@ -183,7 +168,6 @@ const VisualizeChart = ({ scrollResult }) => { if (m.data.error) { enqueueSnackbar(`Visualization Unsuccessful, error: ${m.data.error}`, { variant: 'error', - action, }); } else if (m.data.result && m.data.result.length > 0) { m.data.result.forEach((dataset, index) => { @@ -191,7 +175,7 @@ const VisualizeChart = ({ scrollResult }) => { }); myChart.update(); } else { - enqueueSnackbar(`Visualization Unsuccessful, error: Unexpected Error Occured`, { variant: 'error', action }); + enqueueSnackbar(`Visualization Unsuccessful, error: Unexpected Error Occured`, { variant: 'error' }); } }; diff --git a/src/index.jsx b/src/index.jsx index c888afa1..1d54e29b 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,3 +1,4 @@ +import { Button } from '@mui/material'; import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; @@ -5,7 +6,7 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; import { HashRouter } from 'react-router-dom'; import { ClientProvider } from './context/client-context'; -import { SnackbarProvider } from 'notistack'; +import { SnackbarProvider, closeSnackbar } from 'notistack'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( @@ -18,6 +19,17 @@ root.render( horizontal: 'center', }} style={{ flexWrap: 'nowrap' }} + action={(snackbarId) => ( + + )} > diff --git a/src/lib/graph-visualization-helpers.js b/src/lib/graph-visualization-helpers.js new file mode 100644 index 00000000..4efed78c --- /dev/null +++ b/src/lib/graph-visualization-helpers.js @@ -0,0 +1,52 @@ +export const initGraph = async (qdrantClient, { collectionName, initNode, limit, filter, using }) => { + if (!initNode) { + return { + nodes: [], + links: [], + }; + } + initNode.clicked = true; + + const points = await getSimilarPoints(qdrantClient, { collectionName, pointId: initNode.id, limit, filter, using }); + + const graphData = { + nodes: [initNode, ...points], + links: points.map((point) => ({ source: initNode.id, target: point.id })), + }; + return graphData; +}; + +export const getSimilarPoints = async (qdrantClient, { collectionName, pointId, limit, filter, using }) => { + const { points } = await qdrantClient.query(collectionName, { + query: pointId, + limit: limit, + with_payload: true, + with_vector: false, + filter, + using, + }); + + return points; +}; + +export const getFirstPoint = async (qdrantClient, { collectionName, filter }) => { + const { points } = await qdrantClient.scroll(collectionName, { + limit: 1, + with_payload: true, + with_vector: false, + filter, + }); + + if (!points.length) { + return null; + } + + return points[0]; +}; + +export const deduplicatePoints = (existingPoints, foundPoints) => { + // Returns array of found points that are not in existing points + // deduplication is done by id + const existingIds = new Set(existingPoints.map((point) => point.id)); + return foundPoints.filter((point) => !existingIds.has(point.id)); +}; diff --git a/src/pages/Collection.jsx b/src/pages/Collection.jsx index 914101a3..c379b394 100644 --- a/src/pages/Collection.jsx +++ b/src/pages/Collection.jsx @@ -35,6 +35,7 @@ function Collection() { + diff --git a/src/pages/Graph.jsx b/src/pages/Graph.jsx new file mode 100644 index 00000000..267a9f10 --- /dev/null +++ b/src/pages/Graph.jsx @@ -0,0 +1,211 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { alpha, Paper, Box, Tooltip, Typography, Grid, IconButton } from '@mui/material'; +import { ArrowBack } from '@mui/icons-material'; +import { useTheme } from '@mui/material/styles'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; +import GraphVisualisation from '../components/GraphVisualisation/GraphVisualisation'; +import { useWindowResize } from '../hooks/windowHooks'; +import PointPreview from '../components/GraphVisualisation/PointPreview'; +import CodeEditorWindow from '../components/FilterEditorWindow'; +import { useClient } from '../context/client-context'; +import { getFirstPoint } from '../lib/graph-visualization-helpers'; +import { useSnackbar } from 'notistack'; + +const defaultQuery = ` +// Try me! + +{ + "limit": 5 +} +// Parameters for expansion request: +// +// Available parameters: +// +// - 'limit': number of records to use on each step. +// +// - 'filter': filter expression to select vectors for visualization. +// See https://qdrant.tech/documentation/concepts/filtering/ +// +// - 'using': specify which vector to use for visualization +// if there are multiple. + +`; + +function Graph() { + const theme = useTheme(); + const navigate = useNavigate(); + const params = useParams(); + const [initNode, setInitNode] = useState(null); + const [options, setOptions] = useState({ + limit: 5, + filter: null, + using: null, + collectionName: params.collectionName, + }); + const [visualizeChartHeight, setVisualizeChartHeight] = useState(0); + const VisualizeChartWrapper = useRef(null); + const { height } = useWindowResize(); + const { enqueueSnackbar } = useSnackbar(); + const { client: qdrantClient } = useClient(); + + const [code, setCode] = useState(defaultQuery); + + const [activePoint, setActivePoint] = useState(null); + + useEffect(() => { + setVisualizeChartHeight(height - VisualizeChartWrapper.current?.offsetTop); + }, [height, VisualizeChartWrapper]); + + const handlePointDisplay = useCallback((point) => { + setActivePoint(point); + }, []); + + const handleRunCode = async (data, collectionName) => { + // scroll + try { + const firstPoint = await getFirstPoint(qdrantClient, { collectionName: collectionName, filter: data?.filter }); + setInitNode(firstPoint); + setOptions({ + collectionName: collectionName, + ...data, + }); + } catch (e) { + enqueueSnackbar(e.message, { variant: 'error' }); + } + }; + + const queryRequestSchema = (vectorNames) => ({ + description: 'Filter request', + type: 'object', + properties: { + limit: { + description: 'Page size. Default: 10', + type: 'integer', + format: 'uint', + minimum: 1, + nullable: true, + }, + filter: { + description: 'Look only for points which satisfies this conditions. If not provided - all points.', + anyOf: [ + { + $ref: '#/components/schemas/Filter', + }, + { + nullable: true, + }, + ], + }, + using: { + description: 'Vector field name', + type: 'string', + enum: vectorNames, + }, + }, + }); + + return ( + <> + + + + + + + + + + navigate(`/collections/${params.collectionName}`)} + > + + + + {params.collectionName} + + + + + + + + + + + ⋮ + + + + + + + + + + ⋯ + + + + {activePoint && } + + + + + + + + + ); +} + +export default Graph; diff --git a/src/pages/Visualize.jsx b/src/pages/Visualize.jsx index ad9b3cc7..294cd30b 100644 --- a/src/pages/Visualize.jsx +++ b/src/pages/Visualize.jsx @@ -7,6 +7,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import FilterEditorWindow from '../components/FilterEditorWindow'; import VisualizeChart from '../components/VisualizeChart'; import { useWindowResize } from '../hooks/windowHooks'; +import { requestFromCode } from '../components/FilterEditorWindow/config/RequestFromCode'; const query = ` @@ -68,6 +69,46 @@ function Visualize() { setVisualizeChartHeight(height - VisualizeChartWrapper.current?.offsetTop); }, [height, VisualizeChartWrapper]); + const onEditorCodeRun = async (data, collectionName) => { + const result = await requestFromCode(data, collectionName); + setResult(result); + }; + + const filterRequestSchema = (vectorNames) => ({ + description: 'Filter request', + type: 'object', + properties: { + limit: { + description: 'Page size. Default: 10', + type: 'integer', + format: 'uint', + minimum: 1, + nullable: true, + }, + filter: { + description: 'Look only for points which satisfies this conditions. If not provided - all points.', + anyOf: [ + { + $ref: '#/components/schemas/Filter', + }, + { + nullable: true, + }, + ], + }, + vector_name: { + description: 'Vector field name', + type: 'string', + enum: vectorNames, + }, + color_by: { + description: 'Color points by this field', + type: 'string', + nullable: true, + }, + }, + }); + return ( <> @@ -127,7 +168,12 @@ function Visualize() { - + diff --git a/src/routes.jsx b/src/routes.jsx index 95e36d83..2e6cbdb9 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -7,6 +7,7 @@ import Visualize from './pages/Visualize'; import Tutorial from './pages/Tutorial'; import Datasets from './pages/Datasets'; import Jwt from './pages/Jwt'; +import Graph from './pages/Graph'; const routes = () => [ { @@ -22,6 +23,10 @@ const routes = () => [ path: '/collections/:collectionName/visualize', element: , }, + { + path: '/collections/:collectionName/graph', + element: , + }, { path: '/tutorial', element: }, { path: '/tutorial/:pageSlug', element: }, { path: '/jwt', element: },