Updating UI

This commit is contained in:
James Ketr 2025-03-30 20:37:08 -07:00
parent 179b126130
commit b25498f6a0
6 changed files with 753 additions and 71 deletions

View File

@ -8,6 +8,11 @@
"name": "ketr-chat", "name": "ketr-chat",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.5",
"@mui/icons-material": "^7.0.1",
"@mui/material": "^7.0.1",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
@ -2213,6 +2218,152 @@
"postcss-selector-parser": "^6.0.10" "postcss-selector-parser": "^6.0.10"
} }
}, },
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/@emotion/babel-plugin/node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/styled": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
@ -2309,6 +2460,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@fontsource/roboto": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz",
"integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -2782,6 +2941,241 @@
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="
}, },
"node_modules/@mui/core-downloads-tracker": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.1.tgz",
"integrity": "sha512-T5DNVnSD9pMbj4Jk/Uphz+yvj9dfpl2+EqsOuJtG12HxEihNG5pd3qzX5yM1Id4dDwKRvM3dPVcxyzavTFhJeA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.1.tgz",
"integrity": "sha512-x8Em7LISFQ6s/KeZj6ZKwJHq2WttRNe9KJLWFa72eQx7B53s/TzMKOEjGKB/YyhOx+bqqSv1pMvK373M4Xf07A==",
"dependencies": {
"@babel/runtime": "^7.26.10"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^7.0.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.1.tgz",
"integrity": "sha512-tQwjIIsn/UUSCHoCIQVkANuLua67h7Ro9M9gIHoGWaFbJFuF6cSO4Oda2olDVqIs4SWG+PaDChuu6SngxsaoyQ==",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@mui/core-downloads-tracker": "^7.0.1",
"@mui/system": "^7.0.1",
"@mui/types": "^7.4.0",
"@mui/utils": "^7.0.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.0.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg=="
},
"node_modules/@mui/private-theming": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.1.tgz",
"integrity": "sha512-1kQ7REYjjzDukuMfTbAjm3pLEhD7gUMC2bWhg9VD6f6sHzyokKzX0XHzlr3IdzNWBjPytGkzHpPIRQrUOoPLCQ==",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@mui/utils": "^7.0.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.1.tgz",
"integrity": "sha512-BeGe4xZmF7tESKhmctYrL54Kl25kGHPKVdZYM5qj5Xz76WM/poY+d8EmAqUesT6k2rbJWPp2gtOAXXinNCGunQ==",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.1.tgz",
"integrity": "sha512-pK+puz0hRPHEKGlcPd80mKYD3jpyi0uVIwWffox1WZgPTQMw2dCKLcD+9ndMDJADnrKzmKlpoH756PPFh2UvWA==",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@mui/private-theming": "^7.0.1",
"@mui/styled-engine": "^7.0.1",
"@mui/types": "^7.4.0",
"@mui/utils": "^7.0.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.0.tgz",
"integrity": "sha512-TxJ4ezEeedWHBjOmLtxI203a9DII9l4k83RXmz1PYSAmnyEcK2PglTNmJGxswC/wM5cdl9ap2h8lnXvt2swAGQ==",
"dependencies": {
"@babel/runtime": "^7.26.10"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-SJKrrebNpmK9rJCnVL29nGPhPXQYtBZmb7Dsp0f58uIUhQfAKcBXHE4Kjs06SX4CwqeCuwEVgcHY+MgAO6XQ/g==",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@mui/types": "^7.4.0",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg=="
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1", "version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@ -2898,6 +3292,15 @@
} }
} }
}, },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/plugin-babel": { "node_modules/@rollup/plugin-babel": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -3608,6 +4011,11 @@
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
}, },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
},
"node_modules/@types/q": { "node_modules/@types/q": {
"version": "1.5.8", "version": "1.5.8",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
@ -3639,6 +4047,14 @@
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }
}, },
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -5355,6 +5771,14 @@
"wrap-ansi": "^7.0.0" "wrap-ansi": "^7.0.0"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/co": { "node_modules/co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -6356,6 +6780,15 @@
"utila": "~0.4" "utila": "~0.4"
} }
}, },
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -7732,6 +8165,11 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1" "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
} }
}, },
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@ -8543,6 +8981,19 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/hoopy": { "node_modules/hoopy": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@ -14302,6 +14753,21 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -15885,6 +16351,11 @@
"postcss": "^8.2.15" "postcss": "^8.2.15"
} }
}, },
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/sucrase": { "node_modules/sucrase": {
"version": "3.35.0", "version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",

View File

@ -3,6 +3,11 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.5",
"@mui/icons-material": "^7.0.1",
"@mui/material": "^7.0.1",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",

View File

@ -5,14 +5,27 @@ div {
.App { .App {
display: flex; display: flex;
text-align: center; text-align: center;
max-height: 100%; max-height: 100%;
height: 100%;
flex-direction: column; flex-direction: column;
} }
.Container {
display: flex;
flex-direction: row;
overflow: auto;
margin: 0 auto;
padding: 1rem;
border: 1px solid green;
max-width: 80%;
height: 100vh;
}
.ChatBox { .ChatBox {
display: flex; display: flex;
flex-direction: "row"; flex-direction: column;
flex-grow: 1; flex-grow: 1;
border: 1px solid red;
max-width: 800px;
} }
.Controls { .Controls {
@ -26,39 +39,10 @@ div {
box-sizing: border-box; box-sizing: border-box;
overflow-x: visible; overflow-x: visible;
min-width: 10rem; min-width: 10rem;
}
.container {
display: flex;
flex-grow: 1; flex-grow: 1;
flex-direction: column; border: 1px solid magenta;
height: 100vh;
overflow: auto;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
} }
.query-box {
display: flex;
margin: 20px 0;
}
.query-box input {
flex-grow: 1;
padding: 8px;
margin-right: 10px;
font-size: 1rem;
}
.query-box button,
.user-box button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
.Conversation { .Conversation {
display: flex; display: flex;

View File

@ -1,4 +1,20 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import TextField from '@mui/material/TextField';
import Accordion from '@mui/material/Accordion';
import AccordionActions from '@mui/material/AccordionActions';
import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from "react-spinners/PropagateLoader";
import Markdown from 'react-markdown'; import Markdown from 'react-markdown';
import './App.css'; import './App.css';
@ -6,30 +22,202 @@ import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
const welcomeMessage = { "role": "assistant", "content": "Welcome to Ketr-Chat. I have real-time access to a lot of information. Ask things like 'What are the headlines from cnn.com?' or 'What is the weather in Portland, OR?'" }; const welcomeMessage = { "role": "assistant", "content": "Welcome to Ketr-Chat. I have real-time access to a lot of information. Ask things like 'What are the headlines from cnn.com?' or 'What is the weather in Portland, OR?'" };
const loadingMessage = { "role": "assistant", "content": "Instancing chat session..." }; const loadingMessage = { "role": "assistant", "content": "Instancing chat session..." };
//const url: string = "https://ai.ketrenos.com" //const url: string = "https://ai.ketrenos.com"
const getConnectionBase = (url: string): string => { const getConnectionBase = (loc: any): string => {
if (!url.match(/.*battle-linux.*/)) { if (!loc.host.match(/.*battle-linux.*/)) {
return url; return loc.protocol + "//" + loc.host;
} else { } else {
return 'battle-linux.ketrenos.com:5000'; return loc.protocol + "//battle-linux.ketrenos.com:5000";
} }
} }
const Controls = () => { type Tool = {
const tools = ["get_stock_price", "get_weather", "site_summary", "RAG JPK", "RAG LKML"] label: string,
enabled: boolean
};
interface ControlsParams {
sessionId: string,
connectionBase: string
};
const Controls = ({ sessionId, connectionBase }: ControlsParams) => {
const [systemPrompt, setSystemPrompt] = useState<string>("");
const [editSystemPrompt, setEditSystemPrompt] = useState<string>(systemPrompt);
useEffect(() => {
if (systemPrompt !== "") {
return;
}
const fetchSystemPrompt = async () => {
// Make the fetch request with proper headers
const response = await fetch(connectionBase + `/api/system-prompt/${sessionId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
const data = await response.json();
console.log(data);
setSystemPrompt(data["system-prompt"]);
setEditSystemPrompt(data["system-prompt"]);
}
fetchSystemPrompt();
}, [sessionId, systemPrompt, setSystemPrompt, setEditSystemPrompt, connectionBase]);
const tools: Tool[] = [{
label: "get_stock_price",
enabled: true,
}, {
label: "get_weather",
enabled: true
}, {
label: "site_summary",
enabled: true
}];
const rags: Tool[] = [{
label: "RAG JPK",
enabled: false
}, {
label: "RAG LKML",
enabled: false
}];
const toggleTool = (event: any) => { const toggleTool = (event: any) => {
console.log(`${event.target.value} clicked`)
};
const sendSystemPrompt = async () => {
if (!editSystemPrompt.trim()) {
setEditSystemPrompt(systemPrompt)
return;
}
try {
const response = await fetch(connectionBase + `/api/system-prompt/${sessionId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ "system-prompt": editSystemPrompt.trim() }),
});
const data = await response.json();
console.log(data);
if (data["system-prompt"] !== systemPrompt) {
setSystemPrompt(data["system-prompt"].trim());
}
} catch (error) {
console.error('Fetch error:', error);
}
};
const resetSystemPrompt = async () => {
try {
const response = await fetch(connectionBase + `/api/system-prompt/${sessionId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ "system-prompt": "" }),
});
const data = await response.json();
const systemPrompt = data["system-prompt"].trim();
setSystemPrompt(systemPrompt);
setEditSystemPrompt(systemPrompt)
} catch (error) {
console.error('Fetch error:', error);
}
};
const handleKeyPress = (event: any) => {
if (event.key === 'Enter' && event.ctrlKey) {
switch (event.target.id) {
case 'SystemPromptInput':
sendSystemPrompt();
break;
}
}
}; };
return (<div className="Controls"> return (<div className="Controls">
<div>Enabled Tools</div> { <Accordion>
tools.map((tool, index) => { <AccordionSummary
return (<div key={index}><input type="checkbox" onChange={e => toggleTool(e.target.checked)} />{tool}</div>); expandIcon={<ExpandMoreIcon />}
}) >
}</div>); <Typography component="span">Tools</Typography>
</AccordionSummary>
<AccordionDetails>
These tools can be made available to the LLM for obtaining real-time information from the Internet.
</AccordionDetails>
<AccordionActions>
<FormGroup>
{
tools.map((tool, index) => {
return (<FormControlLabel key={index} control={<Switch checked={tool.enabled} />} onChange={toggleTool} label={tool.label} />);
})
}</FormGroup>
</AccordionActions>
</Accordion>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
>
<Typography component="span">RAG</Typography>
</AccordionSummary>
<AccordionDetails>
These RAG databases can be enabled / disabled for adding additional context based on the chat request.
</AccordionDetails>
<AccordionActions>
<FormGroup>
{
rags.map((rag, index) => {
return (<FormControlLabel key={index} control={<Switch checked={rag.enabled} />} onChange={toggleTool} label={rag.label} />);
})
}</FormGroup>
</AccordionActions>
</Accordion>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
>
<Typography component="span">System Prompt</Typography>
</AccordionSummary>
<AccordionActions style={{ flexDirection: "column" }}>
<TextField
variant="outlined"
autoFocus
fullWidth
multiline
type="text"
value={editSystemPrompt}
onChange={(e) => setEditSystemPrompt(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Enter the new system prompt.."
id="SystemPromptInput"
/>
<div style={{ display: "flex", flexDirection: "row", gap: "8px", paddingTop: "8px" }}>
<Button variant="contained" onClick={sendSystemPrompt}>Set</Button>
<Button variant="outlined" onClick={resetSystemPrompt} color="error">Reset</Button>
</div>
</AccordionActions>
</Accordion>
</div>);
} }
const App = () => { const App = () => {
@ -56,12 +244,12 @@ const App = () => {
}, [sessionId, setConversation]); }, [sessionId, setConversation]);
useEffect(() => { useEffect(() => {
const url = new URL(window.location.href); const url = new URL(loc.href);
const pathParts = url.pathname.split('/').filter(Boolean); const pathParts = url.pathname.split('/').filter(Boolean);
if (!pathParts.length) { if (!pathParts.length) {
console.log("No session id -- creating a new session") console.log("No session id -- creating a new session")
fetch(loc.protocol + "//" + getConnectionBase(loc.host) + `/api/context`, { fetch(getConnectionBase(loc) + `/api/context`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -79,12 +267,12 @@ const App = () => {
setSessionId(pathParts[0]); setSessionId(pathParts[0]);
} }
}, [setSessionId, loc.host, loc.protocol]); }, [setSessionId, loc]);
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyPress = (event: any) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
switch (event.currentTarget.id) { switch (event.target.id) {
case 'query-input': case 'QueryInput':
sendQuery(); sendQuery();
break; break;
} }
@ -121,6 +309,9 @@ const App = () => {
// Clear input // Clear input
setQuery(''); setQuery('');
setTimeout(() => {
document.getElementById("QueryIput")?.focus();
}, 1000);
try { try {
setProcessing(true); setProcessing(true);
@ -135,7 +326,7 @@ const App = () => {
]); ]);
// Make the fetch request with proper headers // Make the fetch request with proper headers
const response = await fetch(loc.protocol + "//" + getConnectionBase(loc.host) + `/api/chat/${sessionId}`, { const response = await fetch(getConnectionBase(loc) + `/api/chat/${sessionId}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -249,7 +440,7 @@ const App = () => {
}; };
return ( return (
<div className="container" style={{ display: "flex", flexDirection: "column" }}> <Container className="Container">
<div className="ChatBox"> <div className="ChatBox">
<div className="Conversation" ref={conversationRef}> <div className="Conversation" ref={conversationRef}>
@ -301,22 +492,28 @@ const App = () => {
/> />
</div> </div>
</div> </div>
<Controls />
</div>
<div className="query-box"> <div className="Query" style={{ display: "flex", flexDirection: "row" }}>
<input <TextField
disabled={processing} variant="outlined"
type="text" disabled={processing}
value={query} autoFocus
onChange={(e) => setQuery(e.target.value)} fullWidth
onKeyDown={handleKeyPress} type="text"
placeholder="Enter your query..." value={query}
id="query-input" onChange={(e) => setQuery(e.target.value)}
/> onKeyDown={handleKeyPress}
<button onClick={sendQuery}>Send</button> placeholder="Enter your question..."
id="QueryInput"
/>
<AccordionActions>
<Button variant="contained" onClick={sendQuery}>Send</Button>
</AccordionActions>
</div>
</div> </div>
</div> {sessionId !== undefined && <Controls sessionId={sessionId} connectionBase={getConnectionBase(loc)} />}
</Container>
); );
}; };

View File

@ -5,10 +5,9 @@ body {
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow: hidden;
padding: 0; padding: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
code { code {

View File

@ -356,6 +356,26 @@ class WebServer:
return RedirectResponse(url=f"/{context['id']}", status_code=307) return RedirectResponse(url=f"/{context['id']}", status_code=307)
#return JSONResponse({"redirect": f"/{context['id']}"}) #return JSONResponse({"redirect": f"/{context['id']}"})
@self.app.put('/api/system-prompt/{context_id}')
async def put_system_prompt(context_id: str, request: Request):
if not is_valid_uuid(context_id):
logging.warning(f"Invalid context_id: {context_id}")
return JSONResponse({"error": "Invalid context_id"}, status_code=400)
context = self.upsert_context(context_id)
data = await request.json()
system_prompt = data["system-prompt"].strip()
if not system_prompt:
system_prompt = system_message
context["system"] = [{"role": "system", "content": system_prompt}]
return JSONResponse({ "system-prompt": system_prompt })
@self.app.get('/api/system-prompt/{context_id}')
async def get_system_prompt(context_id: str):
context = self.upsert_context(context_id)
system_prompt = context["system"][0]["content"];
logging.info(f"returning system prompt as '{system_prompt}'")
return JSONResponse({ "system-prompt": system_prompt })
@self.app.post('/api/chat/{context_id}') @self.app.post('/api/chat/{context_id}')
async def chat_endpoint(context_id: str, request: Request): async def chat_endpoint(context_id: str, request: Request):
context = self.upsert_context(context_id) context = self.upsert_context(context_id)
@ -408,8 +428,9 @@ class WebServer:
self.logging.info(f"Serve index.html for {path}") self.logging.info(f"Serve index.html for {path}")
return FileResponse('/opt/airc/src/ketr-chat/build/index.html') return FileResponse('/opt/airc/src/ketr-chat/build/index.html')
def create_context(self): def create_context(self, context_id = None):
context_id = str(uuid.uuid4()) if not context_id:
context_id = str(uuid.uuid4())
context = { context = {
"id": context_id, "id": context_id,
"system": [{"role": "system", "content": system_message}], "system": [{"role": "system", "content": system_message}],
@ -421,9 +442,14 @@ class WebServer:
return context return context
def upsert_context(self, context_id): def upsert_context(self, context_id):
if not context_id:
logging.warning("No context ID provided. Creating a new context.")
return self.create_context()
if context_id in self.contexts: if context_id in self.contexts:
logging.info(f"Context {context_id} found.")
return self.contexts[context_id] return self.contexts[context_id]
return self.create_context() logging.info(f"Context {context_id} not found. Creating new context.")
return self.create_context(context_id)
async def chat(self, context, content): async def chat(self, context, content):
content = content.strip() content = content.strip()