diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..2ba986f6f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 677781815..d5732fe42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { "name": "chatbox", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "chatbox", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron-forge/maker-dmg": "^6.0.5", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", @@ -54,6 +57,8 @@ "eslint-plugin-import": "^2.25.0", "fork-ts-checker-webpack-plugin": "^7.2.13", "node-loader": "^2.0.0", + "sass": "^1.59.3", + "sass-loader": "^13.2.0", "style-loader": "^3.0.0", "ts-loader": "^9.2.2", "ts-node": "^10.0.0", @@ -224,6 +229,59 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@electron-forge/cli": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-6.0.5.tgz", @@ -1717,6 +1775,338 @@ "@octokit/openapi-types": "^12.11.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@popperjs/core": { "version": "2.11.6", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", @@ -7227,6 +7617,13 @@ "node": ">=6.9.0" } }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -10489,6 +10886,95 @@ "devOptional": true, "license": "MIT" }, + "node_modules/sass": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -12761,6 +13247,41 @@ } } }, + "@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "requires": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + } + }, + "@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "requires": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + } + }, + "@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "requires": { + "tslib": "^2.0.0" + } + }, "@electron-forge/cli": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-6.0.5.tgz", @@ -13780,6 +14301,139 @@ "@octokit/openapi-types": "^12.11.0" } }, + "@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "optional": true, + "requires": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "dependencies": { + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true + }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + } + } + }, + "@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "dev": true, + "optional": true + }, "@popperjs/core": { "version": "2.11.6", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", @@ -17740,6 +18394,12 @@ "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", "optional": true }, + "immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -19933,6 +20593,44 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "devOptional": true }, + "sass": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", + "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "dev": true, + "requires": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "dependencies": { + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true + } + } + }, + "sass-loader": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "dev": true, + "requires": { + "neo-async": "^2.6.2" + } + }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", diff --git a/package.json b/package.json index ea4194e1d..b64f87854 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,9 @@ "typescript": "~4.5.4" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron-forge/maker-dmg": "^6.0.5", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", diff --git a/src/app.tsx b/src/app.tsx index c04042f9d..4407217ac 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,8 +1,13 @@ -import * as ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import App from './devtools/App'; function render() { - ReactDOM.render(, document.body); + const rootElement = document.getElementById('root'); + if (!rootElement) { + throw new Error('Root element not found'); + } + const root = createRoot(rootElement); + root.render(); } render(); diff --git a/src/devtools/App.css b/src/devtools/App.css index 493161332..076ce620f 100644 --- a/src/devtools/App.css +++ b/src/devtools/App.css @@ -1,3 +1,13 @@ .App { text-align: center; } + +/* Drag and drop styles */ +[role="listitem"] { + transition: opacity 200ms ease, transform 200ms ease; +} + +.drag-overlay { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + cursor: grabbing !important; +} \ No newline at end of file diff --git a/src/devtools/App.tsx b/src/devtools/App.tsx index 111026ff7..67b3c796d 100644 --- a/src/devtools/App.tsx +++ b/src/devtools/App.tsx @@ -3,6 +3,7 @@ import './App.css'; import Block from './Block' import * as client from './client' import SessionItem from './SessionItem' +import SortableSessionItem from './SortableSessionItem' import { Toolbar, Box, Badge, Snackbar, List, ListSubheader, ListItemText, MenuList, @@ -22,12 +23,60 @@ import * as prompts from './prompts' import CleaningServicesIcon from '@mui/icons-material/CleaningServices'; import CleanWidnow from './CleanWindow'; import { ThemeSwitcherProvider } from './theme/ThemeSwitcher'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, + DragStartEvent, + DragOverlay, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; const { useEffect, useState } = React function Main() { const store = useStore() + // Drag and drop sensors + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const [activeId, setActiveId] = useState(null); + const [draggedSession, setDraggedSession] = useState(null); + + const handleDragStart = (event: DragStartEvent) => { + setActiveId(event.active.id as string); + const session = store.chatSessions.find(s => s.id === event.active.id); + setDraggedSession(session || null); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (over && active.id !== over.id) { + store.reorderSessions(active.id as string, over.id as string); + } + setActiveId(null); + setDraggedSession(null); + }; + + const handleDragCancel = () => { + setActiveId(null); + setDraggedSession(null); + }; + // 是否展示设置窗口 const [openSettingWindow, setOpenSettingWindow] = React.useState(false); useEffect(() => { @@ -151,16 +200,24 @@ function Main() { }}> @@ -192,24 +249,53 @@ function Main() { } > - { - store.chatSessions.map((session, ix) => ( - { - store.switchCurrentSession(session) - document.getElementById('message-input')?.focus() // better way? - }} - deleteMe={() => store.deleteChatSession(session)} - copyMe={() => { - const newSession = createSession(session.name + ' Copyed') - newSession.messages = session.messages - store.createChatSession(newSession, ix) - }} - editMe={() => setConfigureChatConfig(session)} - /> - )) - } + + s.id)} + strategy={verticalListSortingStrategy} + > + { + store.chatSessions.map((session, ix) => ( + { + store.switchCurrentSession(session) + document.getElementById('message-input')?.focus() // better way? + }} + deleteMe={() => store.deleteChatSession(session)} + copyMe={() => { + const newSession = createSession(session.name + ' Copyed') + newSession.messages = session.messages + store.createChatSession(newSession, ix) + }} + editMe={() => setConfigureChatConfig(session)} + /> + )) + } + + + {draggedSession ? ( + + {}} + deleteMe={() => {}} + copyMe={() => {}} + editMe={() => {}} + /> + + ) : null} + + @@ -263,6 +349,8 @@ function Main() { { - if (event.keyCode === 13 && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { + if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { event.preventDefault() submit() return diff --git a/src/devtools/Block.tsx b/src/devtools/Block.tsx index 6f6f50174..1e151c106 100644 --- a/src/devtools/Block.tsx +++ b/src/devtools/Block.tsx @@ -21,6 +21,8 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import * as wordCount from './utils' import FormatQuoteIcon from '@mui/icons-material/FormatQuote'; +import CopyButton from './CopyButton'; +import MarkdownRenderer from './MarkdownRenderer'; import 'github-markdown-css/github-markdown-light.css' import mila from 'markdown-it-link-attributes' @@ -39,7 +41,7 @@ const md = new MarkdownIt({ } else { content = md.utils.escapeHtml(str) } - return `${content}`; + return `${content}`; } }); md.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' }) @@ -150,11 +152,8 @@ function _Block(props: Props) { id={msg.id + 'input'} /> ) : ( - ) } diff --git a/src/devtools/CopyButton.tsx b/src/devtools/CopyButton.tsx new file mode 100644 index 000000000..4ae9735dc --- /dev/null +++ b/src/devtools/CopyButton.tsx @@ -0,0 +1,105 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { IconButton, Tooltip, Snackbar, Alert } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import CheckIcon from '@mui/icons-material/Check'; +import { styled } from '@mui/material/styles'; + +const CopyButtonContainer = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'inline-block', + width: '100%', + '&:hover .copy-button': { + opacity: 1, + visibility: 'visible', + }, +})); + +const CopyButtonOverlay = styled(IconButton)(({ theme }) => ({ + position: 'absolute', + top: theme.spacing(1), + right: theme.spacing(1), + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + opacity: 0, + visibility: 'hidden', + transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out', + zIndex: 10, + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + '&.copied': { + color: theme.palette.success.main, + }, +})); + +interface CopyButtonProps { + content: string; + className?: string; +} + +export const CopyButton: React.FC = ({ content, className = '' }) => { + const [copied, setCopied] = useState(false); + const [showSnackbar, setShowSnackbar] = useState(false); + const containerRef = useRef(null); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(content); + setCopied(true); + setShowSnackbar(true); + + // Reset the copied state after 2 seconds + setTimeout(() => { + setCopied(false); + }, 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = content; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand('copy'); + setCopied(true); + setShowSnackbar(true); + setTimeout(() => { + setCopied(false); + }, 2000); + } catch (fallbackErr) { + console.error('Fallback copy failed: ', fallbackErr); + } + document.body.removeChild(textArea); + } + }; + + const handleSnackbarClose = () => { + setShowSnackbar(false); + }; + + return ( + <> + + + {copied ? : } + + + + Copied to clipboard! + + + + > + ); +}; + +export default CopyButton; diff --git a/src/devtools/MarkdownRenderer.tsx b/src/devtools/MarkdownRenderer.tsx new file mode 100644 index 000000000..2f46c4666 --- /dev/null +++ b/src/devtools/MarkdownRenderer.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Box } from '@mui/material'; +import { IconButton, Tooltip } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import CheckIcon from '@mui/icons-material/Check'; +import { styled } from '@mui/material/styles'; + +const CodeBlockWrapper = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'inline-block', + width: '100%', + '&:hover .copy-button': { + opacity: 1, + visibility: 'visible', + }, +})); + +const CopyButtonOverlay = styled(IconButton)(({ theme }) => ({ + position: 'absolute', + top: theme.spacing(1), + right: theme.spacing(1), + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + opacity: 0, + visibility: 'hidden', + transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out', + zIndex: 10, + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + '&.copied': { + color: theme.palette.success.main, + }, +})); + +interface MarkdownRendererProps { + content: string; + className?: string; +} + +export const MarkdownRenderer: React.FC = ({ content, className }) => { + const containerRef = useRef(null); + const [copiedStates, setCopiedStates] = useState<{ [key: string]: boolean }>({}); + + useEffect(() => { + if (containerRef.current) { + // Find all code block wrappers and add copy buttons + const codeWrappers = containerRef.current.querySelectorAll('.code-block-wrapper'); + codeWrappers.forEach((wrapper, index) => { + const codeElement = wrapper.querySelector('code'); + if (codeElement && !wrapper.hasAttribute('data-copy-processed')) { + const codeText = codeElement.textContent || ''; + const wrapperId = `code-block-${index}`; + + // Create copy button + const copyButton = document.createElement('div'); + copyButton.className = 'copy-button'; + copyButton.innerHTML = ` + + + + + Copy + + `; + + wrapper.appendChild(copyButton); + wrapper.setAttribute('data-copy-processed', 'true'); + wrapper.setAttribute('data-wrapper-id', wrapperId); + + // Add hover effect + wrapper.style.position = 'relative'; + wrapper.style.display = 'inline-block'; + wrapper.style.width = '100%'; + } + }); + + // Add event listeners for copy buttons + const copyButtons = containerRef.current.querySelectorAll('.copy-button button'); + copyButtons.forEach((button) => { + button.addEventListener('click', async (e) => { + e.preventDefault(); + const target = e.target as HTMLElement; + const codeText = target.getAttribute('data-code-text') || ''; + const wrapper = target.closest('.code-block-wrapper'); + const wrapperId = wrapper?.getAttribute('data-wrapper-id') || ''; + + try { + await navigator.clipboard.writeText(codeText); + setCopiedStates(prev => ({ ...prev, [wrapperId]: true })); + + // Update button appearance + target.innerHTML = ` + + + + Copied! + `; + target.style.color = '#4caf50'; + + // Reset after 2 seconds + setTimeout(() => { + setCopiedStates(prev => ({ ...prev, [wrapperId]: false })); + target.innerHTML = ` + + + + Copy + `; + target.style.color = ''; + }, 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + }); + }); + } + }, [content]); + + return ( + + ); +}; + +export default MarkdownRenderer; diff --git a/src/devtools/SessionItem.tsx b/src/devtools/SessionItem.tsx index ebcadfc21..a5eb6af14 100644 --- a/src/devtools/SessionItem.tsx +++ b/src/devtools/SessionItem.tsx @@ -24,10 +24,12 @@ export interface Props { deleteMe: () => void copyMe: () => void editMe: () => void + dragHandleProps?: any + isDragging?: boolean } export default function SessionItem(props: Props) { - const { session, selected, switchMe, deleteMe, copyMe, editMe } = props + const { session, selected, switchMe, deleteMe, copyMe, editMe, dragHandleProps, isDragging } = props const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { @@ -38,14 +40,28 @@ export default function SessionItem(props: Props) { setAnchorEl(null); }; + const handleClickWrapper = (e: React.MouseEvent) => { + if (isDragging) { + e.preventDefault(); + e.stopPropagation(); + } + }; + return ( switchMe()} + selected={selected && !isDragging} + onClick={switchMe} + {...(dragHandleProps || {})} + style={{ + cursor: isDragging ? 'grabbing' : (dragHandleProps ? 'grab' : 'pointer'), + ...(isDragging ? { opacity: 0.5 } : {}), + userSelect: 'none' + }} + onPointerDown={handleClickWrapper} > - - + + diff --git a/src/devtools/SortableSessionItem.tsx b/src/devtools/SortableSessionItem.tsx new file mode 100644 index 000000000..a9ca6326f --- /dev/null +++ b/src/devtools/SortableSessionItem.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import SessionItem from './SessionItem'; +import { Session } from './types'; + +interface SortableSessionItemProps { + session: Session + selected: boolean + switchMe: () => void + deleteMe: () => void + copyMe: () => void + editMe: () => void +} + +export default function SortableSessionItem(props: SortableSessionItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: props.session.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( + + + + ); +} + diff --git a/src/devtools/store.ts b/src/devtools/store.ts index 678485525..a62541a0f 100644 --- a/src/devtools/store.ts +++ b/src/devtools/store.ts @@ -110,6 +110,16 @@ export default function useStore() { const [chatSessions, _setChatSessions] = useState([createSession()]) const [currentSession, switchCurrentSession] = useState(chatSessions[0]) + + // Sync currentSession with chatSessions - if currentSession was deleted, switch to a valid one + useEffect(() => { + const currentExists = chatSessions.some(s => s.id === currentSession.id) + if (!currentExists && chatSessions.length > 0) { + // Current session was deleted, switch to first available session + switchCurrentSession(chatSessions[0]) + } + }, [chatSessions]) // Only depend on chatSessions to avoid infinite loop + useEffect(() => { readSessions().then((sessions: Session[]) => { _setChatSessions(sessions) @@ -122,16 +132,95 @@ export default function useStore() { } const deleteChatSession = (target: Session) => { + // Find the index of the session being deleted + const deletedIndex = chatSessions.findIndex((s) => s.id === target.id) + + // Filter out the deleted session const sessions = chatSessions.filter((s) => s.id !== target.id) + + // Ensure there's at least one session if (sessions.length === 0) { sessions.push(createSession()) } + + // Handle session switching based on what was deleted if (target.id === currentSession.id) { - switchCurrentSession(sessions[0]) + // Deleted session was active - navigate to a predictable location + let nextSession: Session | null = null + + // Prefer the previous session (before the deleted one) + if (deletedIndex > 0) { + // The previous session is at deletedIndex - 1 in the original array + const previousSessionId = chatSessions[deletedIndex - 1].id + nextSession = sessions.find((s) => s.id === previousSessionId) || null + } + + // If no previous session, try the next one + if (!nextSession && deletedIndex < chatSessions.length - 1) { + // The next session was at deletedIndex + 1 in the original array + const nextSessionId = chatSessions[deletedIndex + 1].id + nextSession = sessions.find((s) => s.id === nextSessionId) || null + } + + // Fallback to first available session + if (!nextSession) { + nextSession = sessions[0] + } + + // Ensure we have a valid session to switch to + if (!nextSession) { + // This should never happen since we ensure at least one session exists above + console.error('No session to switch to after deletion') + return + } + + // Update sessions array and switch session - must happen synchronously + // Switch first to immediately update currentSession, then update chatSessions + switchCurrentSession(nextSession) + _setChatSessions(sessions) + writeSessions(sessions) + } else { + // Deleting a non-active session - stay on current session + // Verify the current session still exists in the filtered array + const currentSessionStillExists = sessions.some((s) => s.id === currentSession.id) + if (currentSessionStillExists) { + // Current session still exists - update sessions array and refresh currentSession reference + // Get the updated session reference from the new array to ensure consistency + const updatedCurrentSession = sessions.find((s) => s.id === currentSession.id)! + _setChatSessions(sessions) + writeSessions(sessions) + // Update currentSession reference to match the new array (even though it's the same session) + // This ensures we're always using a reference that exists in chatSessions + switchCurrentSession(updatedCurrentSession) + } else { + // Edge case: current session was deleted somehow - shouldn't happen + // Fallback to first available session + switchCurrentSession(sessions[0]) + _setChatSessions(sessions) + writeSessions(sessions) + } } - setSessions(sessions) + } + const reorderSessions = (activeId: string, overId: string | null) => { + if (!overId) return + const oldIndex = chatSessions.findIndex(s => s.id === activeId) + const newIndex = chatSessions.findIndex(s => s.id === overId) + if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return + + const newSessions = [...chatSessions] + const [movedItem] = newSessions.splice(oldIndex, 1) + newSessions.splice(newIndex, 0, movedItem) + setSessions(newSessions) } const updateChatSession = (session: Session) => { + // Check if the session exists in chatSessions - if not, it was deleted + const sessionExists = chatSessions.some((s) => s.id === session.id) + if (!sessionExists) { + // Session doesn't exist - it was deleted, don't allow updates + console.warn('Attempted to update deleted session:', session.id) + return + } + const sessions = chatSessions.map((s) => { if (s.id === session.id) { return session @@ -180,6 +269,7 @@ export default function useStore() { updateChatSession, deleteChatSession, createEmptyChatSession, + reorderSessions, currentSession, switchCurrentSession, diff --git a/src/index.html b/src/index.html index f8befb0d7..f760e47d4 100644 --- a/src/index.html +++ b/src/index.html @@ -5,8 +5,6 @@ ChatBox - - ChatBox - +
${content}