diff --git a/.gitignore b/.gitignore index 46763dacc..183c55563 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ node_modules node_modules/ dist/ .vercel + +*.swp \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eecea3805..419790174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ "@popperjs/core": "^2.11.6", "@sentry/svelte": "^7.44.2", "@sentry/tracing": "^7.44.2", + "ai": "^2.1.15", "analytics": "^0.8.1", "dayjs": "^1.11.9", "dotenv": "^16.0.3", "echarts": "^5.4.1", "logrocket": "^3.0.1", + "nanoid": "^4.0.2", "pretty-bytes": "^6.1.0", "prismjs": "^1.29.0", "svelte-confetti": "^1.2.2", @@ -544,7 +546,6 @@ "version": "7.22.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", - "dev": true, "peer": true, "bin": { "parser": "bin/babel-parser.js" @@ -2307,6 +2308,118 @@ "pretty-format": "^27.5.1" } }, + "node_modules/@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "peer": true, + "dependencies": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "peer": true, + "dependencies": { + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "peer": true, + "dependencies": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "peer": true, + "dependencies": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + }, + "peerDependencies": { + "vue": "3.3.4" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", + "peer": true + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -2365,6 +2478,71 @@ "node": ">= 6.0.0" } }, + "node_modules/ai": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/ai/-/ai-2.1.32.tgz", + "integrity": "sha512-G+mk7OfSxVR5s2cmpni89olZjXo5XbxPzzG8DAZThoJA2725hczfvO/gqLYGUToq92vzr0XgCHP6JJFK9so5fg==", + "dependencies": { + "eventsource-parser": "1.0.0", + "nanoid": "3.3.6", + "solid-swr-store": "0.10.7", + "sswr": "2.0.0", + "swr": "2.2.0", + "swr-store": "0.10.6", + "swrv": "1.0.4" + }, + "engines": { + "node": ">=14.6" + }, + "peerDependencies": { + "react": "^18.2.0", + "solid-js": "^1.7.7", + "svelte": "^3.0.0 || ^4.0.0", + "vue": "^3.3.4" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/ai/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ai/node_modules/sswr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz", + "integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==", + "dependencies": { + "swrev": "^4.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3109,6 +3287,12 @@ "node": ">=14" } }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "peer": true + }, "node_modules/data-urls": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", @@ -3238,6 +3422,14 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -3735,6 +3927,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "peer": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3744,6 +3942,14 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", + "integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==", + "engines": { + "node": ">=14.18" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5700,8 +5906,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5908,6 +6113,18 @@ "resolved": "https://registry.npmjs.org/logrocket/-/logrocket-3.0.1.tgz", "integrity": "sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ==" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -6132,10 +6349,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", "funding": [ { "type": "github", @@ -6143,10 +6359,10 @@ } ], "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^14 || ^16 || >=18" } }, "node_modules/natural-compare": { @@ -6493,8 +6709,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6614,7 +6829,6 @@ "version": "8.4.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6638,6 +6852,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/pre-commit": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", @@ -6883,6 +7114,18 @@ } ] }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7214,6 +7457,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/seroval": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", + "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -7326,6 +7578,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/solid-js": { + "version": "1.7.10", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.10.tgz", + "integrity": "sha512-myc4UeC1DeQl4jfcA1bF0X4fUn3tDfHz7QGFMN9PJ77ldG7zzD0CtDz1a2u27gPBSFNjyNL/mJ0N9/6uCuh63A==", + "peer": true, + "dependencies": { + "csstype": "^3.1.0", + "seroval": "^0.5.0" + } + }, + "node_modules/solid-swr-store": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz", + "integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "solid-js": "^1.2", + "swr-store": "^0.10" + } + }, "node_modules/sorcery": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", @@ -7354,7 +7628,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7736,6 +8009,41 @@ "node": ">=12" } }, + "node_modules/swr": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", + "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", + "dependencies": { + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/swr-store": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz", + "integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==", + "dependencies": { + "dequal": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz", + "integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==" + }, + "node_modules/swrv": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.0.4.tgz", + "integrity": "sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==", + "peerDependencies": { + "vue": ">=3.2.26 < 4" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -8020,6 +8328,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8217,6 +8533,19 @@ } } }, + "node_modules/vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 8848043e3..0abc8bb6b 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,13 @@ "@popperjs/core": "^2.11.6", "@sentry/svelte": "^7.44.2", "@sentry/tracing": "^7.44.2", + "ai": "^2.1.15", "analytics": "^0.8.1", "dayjs": "^1.11.9", "dotenv": "^16.0.3", "echarts": "^5.4.1", "logrocket": "^3.0.1", + "nanoid": "^4.0.2", "pretty-bytes": "^6.1.0", "prismjs": "^1.29.0", "svelte-confetti": "^1.2.2", diff --git a/src/lib/actions/portal.ts b/src/lib/actions/portal.ts new file mode 100644 index 000000000..05531b47c --- /dev/null +++ b/src/lib/actions/portal.ts @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import type { Action } from 'svelte/action'; + +export type PortalConfig = string | HTMLElement | undefined; + +export const portal: Action = (el, target = 'body') => { + let targetEl; + + async function update(newTarget: PortalConfig) { + target = newTarget; + if (typeof target === 'string') { + targetEl = document.querySelector(target); + if (targetEl === null) { + await tick(); + targetEl = document.querySelector(target); + } + if (targetEl === null) { + throw new Error(`No element found matching css selector: "${target}"`); + } + } else if (target instanceof HTMLElement) { + targetEl = target; + } else { + throw new TypeError( + `Unknown portal target type: ${ + target === null ? 'null' : typeof target + }. Allowed types: string (CSS selector) or HTMLElement.` + ); + } + targetEl.appendChild(el); + el.hidden = false; + } + + function destroy() { + el.remove(); + } + + update(target); + return { + update, + destroy + }; +}; diff --git a/src/lib/commandCenter/commandCenter.svelte b/src/lib/commandCenter/commandCenter.svelte new file mode 100644 index 000000000..3dcf142f9 --- /dev/null +++ b/src/lib/commandCenter/commandCenter.svelte @@ -0,0 +1,142 @@ + + + + + + +{#if openSubPanel} +
+ +
+{/if} + +{#if dev} +
+ {#each keys as key, i (i)} + + {key.length === 1 ? key.toUpperCase() : key} + + {/each} +
+{/if} + + diff --git a/src/lib/commandCenter/commands.ts b/src/lib/commandCenter/commands.ts new file mode 100644 index 000000000..734a3ab6a --- /dev/null +++ b/src/lib/commandCenter/commands.ts @@ -0,0 +1,349 @@ +import { debounce } from '$lib/helpers/debounce'; +import { isMac } from '$lib/helpers/platform'; +import { wizard } from '$lib/stores/wizard'; +import { onMount } from 'svelte'; +import { derived, writable } from 'svelte/store'; +import { nanoid } from 'nanoid/non-secure'; +import { trackEvent } from '$lib/actions/analytics'; + +const groups = [ + 'ungrouped', + 'navigation', + 'projects', + 'organizations', + 'auth', + 'help', + 'account', + 'platforms', + 'databases', + 'functions', + 'storage', + 'domains', + 'webhooks', + 'integrations', + 'migrations', + 'users', + 'collections', + 'attributes', + 'indexes', + 'documents', + 'teams', + 'security', + 'buckets', + 'files', + 'misc' +] as const; + +export type CommandGroup = (typeof groups)[number]; + +type BaseCommand = { + callback: () => void; + label?: string; + disabled?: boolean; + forceEnable?: boolean; + group?: CommandGroup; + icon?: string; + rank?: number; + nested?: boolean; + keepOpen?: boolean; +}; + +type KeyedCommand = BaseCommand & { + keys: string[]; + /* Ctrl on Windows/Linux, Meta on Mac */ + ctrl?: boolean; + shift?: boolean; + /* Alt on Windows/Linux, Option on Mac */ + alt?: boolean; +}; + +function isKeyedCommand(command: Command): command is KeyedCommand { + return 'keys' in command; +} + +export type Command = KeyedCommand | BaseCommand; + +export const commandMap = writable>(new Map()); +export const disabledMap = writable>(new Map()); + +// Derived stores +export const commands = derived(commandMap, ($commandMap) => { + return Array.from($commandMap.values()).flat(); +}); + +const commandsEnabled = derived(disabledMap, ($disabledMap) => { + // If there's an item on the disabledMap that's true, then disable the command center + return Array.from($disabledMap.values()).every((disabled) => !disabled); +}); + +function isInputEvent(event: KeyboardEvent) { + return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName); +} + +function getCommandRank(command: KeyedCommand) { + const { keys, ctrl: meta, shift, alt } = command; + const modifiers = [meta, shift, alt].filter(Boolean).length; + return keys.length + modifiers * 10; +} + +function hasDisputing(command: KeyedCommand, allCommands: Command[]) { + return allCommands.some((otherCommand) => { + if (command === otherCommand) { + return false; + } + if (!isKeyedCommand(otherCommand)) { + return false; + } + const keysString = command.keys.join('+'); + const otherKeysString = otherCommand.keys.join('+'); + + const cmdRank = getCommandRank(command); + const otherCmdRank = getCommandRank(otherCommand); + + return ( + (keysString.includes(otherKeysString) || otherKeysString.includes(keysString)) && + cmdRank <= otherCmdRank + ); + }); +} + +export const commandCenterKeyDownHandler = derived( + [commandMap, commandsEnabled, wizard], + ([$commandMap, enabled, $wizard]) => { + const commandsArr = Array.from($commandMap.values()).flat(); + let recentKeyCodes: number[] = []; + let validCommands: KeyedCommand[] = []; + + const reset = debounce(() => { + recentKeyCodes = []; + validCommands = []; + }, 1000); + + const getHighestPriorityCommand = () => { + if (!validCommands.length) return; + + if (validCommands.length === 1) { + return validCommands[0]; + } + // Rank commands by how many keys and modifiers they have. + // Each key is worth 1 point, each modifier is worth 10 points. + // The command with the highest score wins. + const rankedCommands = validCommands.map((command) => { + return { command, score: getCommandRank(command) }; + }); + + const highestScore = Math.max(...rankedCommands.map(({ score }) => score)); + const highestScoreCommands = rankedCommands.filter( + ({ score }) => score === highestScore + ); + + if (highestScoreCommands.length === 1) { + return highestScoreCommands[0].command; + } + + // If there's still a tie, the command with the most modifiers wins. + // And if even that's a tie, the first command wins. + const mostModifiers = Math.max( + ...highestScoreCommands.map(({ command }) => { + const { ctrl: meta, shift, alt } = command; + return [meta, shift, alt].filter(Boolean).length; + }) + ); + const mostModifiersCommands = highestScoreCommands.filter(({ command }) => { + const { ctrl: meta, shift, alt } = command; + return [meta, shift, alt].filter(Boolean).length === mostModifiers; + }); + + return mostModifiersCommands[0]?.command; + }; + + const rankAndExecute = debounce(() => { + const command = getHighestPriorityCommand(); + command?.callback(); + reset.immediate(); + }, 200); + + const execute = (command: KeyedCommand) => { + if (hasDisputing(command, commandsArr)) { + validCommands.push(command); + rankAndExecute(); + } else { + command.callback(); + reset.immediate(); + } + }; + + return (event: KeyboardEvent) => { + recentKeyCodes.push(event.keyCode); + reset(); + + for (const command of commandsArr) { + if (!isKeyedCommand(command)) continue; + if (!command.forceEnable) { + if (command.disabled || !enabled || isInputEvent(event) || $wizard.show) { + continue; + } + } + + const { keys, ctrl: meta, shift, alt } = command; + + const isMetaPressed = meta + ? isMac() + ? event.metaKey + : event.ctrlKey + : !(isMac() ? event.metaKey : event.ctrlKey); + const isShiftPressed = shift ? event.shiftKey : !event.shiftKey; + const isAltPressed = alt ? event.altKey : !event.altKey; + + const commandKeyCodes = keys.map((key) => key.toUpperCase().charCodeAt(0)); + const allKeysPressed = recentKeyCodes.join('').includes(commandKeyCodes.join('')); + + if (allKeysPressed && isMetaPressed && isShiftPressed && isAltPressed) { + event.preventDefault(); + execute(command); + } + } + }; + } +); + +// Methods +export const registerCommands = { + subscribe(runner: (cb: (newCommands: Command[]) => void) => void) { + const uuid = nanoid(); + + runner((newCommands: Command[]) => { + commandMap.update((curr) => { + const commandsWithTracking = newCommands.map((command) => { + const trackingCallback = () => { + if (command.label) { + trackEvent('command', { label: command.label, group: command.group }); + } + command.callback(); + }; + + return { ...command, callback: trackingCallback }; + }); + + curr.set(uuid, commandsWithTracking); + return curr; + }); + }); + + return () => { + commandMap.update((curr) => { + curr.delete(uuid); + return curr; + }); + }; + } +}; + +export const disableCommands = { + subscribe(runner: (cb: (disabled: boolean) => void) => void) { + const uuid = nanoid(); + + runner((disabled: boolean) => { + disabledMap.update((curr) => { + curr.set(uuid, disabled); + return curr; + }); + }); + + return () => { + disabledMap.update((curr) => { + curr.delete(uuid); + return curr; + }); + }; + } +}; + +type CommandGroupRanks = Partial>; + +type GroupRanksMap = Map; +const groupRanksMap = writable(new Map()); + +export const updateCommandGroupRanks = { + subscribe(runner: (cb: (updater: CommandGroupRanks) => void) => void) { + const uuid = nanoid(); + + runner((groupRank: CommandGroupRanks) => { + groupRanksMap.update((curr) => { + curr.set(uuid, groupRank); + return curr; + }); + }); + + return () => { + groupRanksMap.update((curr) => { + curr.delete(uuid); + return curr; + }); + }; + } +}; + +export const commandGroupRanks = derived(groupRanksMap, ($groupRankTransformations) => { + const initialRanks = { + ...Object.fromEntries(groups.map((group) => [group, 0])), + ungrouped: 9999, + databases: 3, + users: 2, + teams: 1, + navigation: -10, + help: -20, + misc: -30 + } as CommandGroupRanks; + + const transformations = Array.from($groupRankTransformations.values()); + return transformations.reduce((prev, curr) => ({ ...prev, ...curr }), initialRanks); +}); + +export type Searcher = (query: string) => Promise; +const searchersMap = writable>(new Map()); + +export const registerSearchers = { + subscribe(runner: (cb: (...searchers: Searcher[]) => void) => void) { + const uuid = nanoid(); + + runner((...searchers: Searcher[]) => { + searchersMap.update((curr) => { + curr.set(uuid, [...searchers]); + return curr; + }); + }); + + return () => { + searchersMap.update((curr) => { + curr.delete(uuid); + return curr; + }); + }; + } +}; + +export const searchers = derived(searchersMap, ($searchersMap) => { + return Array.from($searchersMap.values()).flat(); +}); + +export const initSearcher = (searcher: Searcher) => { + const search = writable(''); + const results = writable([]); + + const searcherDebounced = debounce(async (query: string) => { + results.set(await searcher(query)); + }, 500); + + onMount(() => { + searcherDebounced.immediate(''); + return search.subscribe((query) => { + searcherDebounced(query); + }); + }); + + return { + search, + results + }; +}; diff --git a/src/lib/commandCenter/index.ts b/src/lib/commandCenter/index.ts new file mode 100644 index 000000000..b1d8ce511 --- /dev/null +++ b/src/lib/commandCenter/index.ts @@ -0,0 +1,4 @@ +export * from './commands'; +export * from './subPanels'; +import CommandCenter from './commandCenter.svelte'; +export { CommandCenter }; diff --git a/src/lib/commandCenter/panels/ai.svelte b/src/lib/commandCenter/panels/ai.svelte new file mode 100644 index 000000000..3cc6a5409 --- /dev/null +++ b/src/lib/commandCenter/panels/ai.svelte @@ -0,0 +1,227 @@ + + + + + diff --git a/src/lib/commandCenter/panels/buckets.svelte b/src/lib/commandCenter/panels/buckets.svelte new file mode 100644 index 000000000..06859101c --- /dev/null +++ b/src/lib/commandCenter/panels/buckets.svelte @@ -0,0 +1,9 @@ + + +