mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
feat: add experimental Chat UI (#17650)
Builds on https://github.com/coder/coder/pull/17570 Frontend portion of https://github.com/coder/coder/tree/chat originally authored by @kylecarbs Additional changes: - Addresses linter complaints - Brings `ChatToolInvocation` argument definitions in line with those defined in `codersdk/toolsdk` - Ensures chat-related features are not shown unless `ExperimentAgenticChat` is enabled. Co-authored-by: Kyle Carberry <kyle@carberry.com>
This commit is contained in:
@ -35,6 +35,8 @@
|
||||
"update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "2.2.6",
|
||||
"@ai-sdk/react": "1.2.6",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@emotion/cache": "11.14.0",
|
||||
@ -111,6 +113,7 @@
|
||||
"react-virtualized-auto-sizer": "1.0.24",
|
||||
"react-window": "1.8.11",
|
||||
"recharts": "2.15.0",
|
||||
"rehype-raw": "7.0.0",
|
||||
"remark-gfm": "4.0.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
|
216
site/pnpm-lock.yaml
generated
216
site/pnpm-lock.yaml
generated
@ -16,6 +16,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils':
|
||||
specifier: 2.2.6
|
||||
version: 2.2.6(zod@3.24.3)
|
||||
'@ai-sdk/react':
|
||||
specifier: 1.2.6
|
||||
version: 1.2.6(react@18.3.1)(zod@3.24.3)
|
||||
'@emoji-mart/data':
|
||||
specifier: 1.2.1
|
||||
version: 1.2.1
|
||||
@ -244,6 +250,9 @@ importers:
|
||||
recharts:
|
||||
specifier: 2.15.0
|
||||
version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
rehype-raw:
|
||||
specifier: 7.0.0
|
||||
version: 7.0.0
|
||||
remark-gfm:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
@ -489,6 +498,42 @@ packages:
|
||||
'@adobe/css-tools@4.4.1':
|
||||
resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==, tarball: https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz}
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.4':
|
||||
resolution: {integrity: sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.4.tgz}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.6':
|
||||
resolution: {integrity: sha512-sUlZ7Gnq84DCGWMQRIK8XVbkzIBnvPR1diV4v6JwPgpn5armnLI/j+rqn62MpLrU5ZCQZlDKl/Lw6ed3ulYqaA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.6.tgz}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@ai-sdk/provider@1.1.0':
|
||||
resolution: {integrity: sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/provider@1.1.2':
|
||||
resolution: {integrity: sha512-ITdgNilJZwLKR7X5TnUr1BsQW6UTX5yFp0h66Nfx8XjBYkWD9W3yugr50GOz3CnE9m/U/Cd5OyEbTMI0rgi6ZQ==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.2.tgz}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.2.6':
|
||||
resolution: {integrity: sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ==, tarball: https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.6.tgz}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.5':
|
||||
resolution: {integrity: sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg==, tarball: https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.5.tgz}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, tarball: https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz}
|
||||
engines: {node: '>=10'}
|
||||
@ -3942,18 +3987,33 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==, tarball: https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz}
|
||||
|
||||
hast-util-parse-selector@2.2.5:
|
||||
resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz}
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==, tarball: https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.2:
|
||||
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==, tarball: https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz}
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==, tarball: https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==, tarball: https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz}
|
||||
|
||||
hastscript@6.0.0:
|
||||
resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz}
|
||||
|
||||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz}
|
||||
|
||||
headers-polyfill@4.0.3:
|
||||
resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, tarball: https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz}
|
||||
|
||||
@ -3976,6 +4036,9 @@ packages:
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==, tarball: https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==, tarball: https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz}
|
||||
|
||||
http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, tarball: https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -4480,6 +4543,9 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz}
|
||||
|
||||
json-schema@0.4.0:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz}
|
||||
|
||||
@ -5236,6 +5302,9 @@ packages:
|
||||
property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==, tarball: https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz}
|
||||
|
||||
property-information@7.0.0:
|
||||
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==, tarball: https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz}
|
||||
|
||||
protobufjs@7.4.0:
|
||||
resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==, tarball: https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -5492,6 +5561,9 @@ packages:
|
||||
resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==, tarball: https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==, tarball: https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz}
|
||||
|
||||
remark-gfm@4.0.0:
|
||||
resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz}
|
||||
|
||||
@ -5599,6 +5671,9 @@ packages:
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, tarball: https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==, tarball: https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz}
|
||||
|
||||
semver@7.6.2:
|
||||
resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==, tarball: https://registry.npmjs.org/semver/-/semver-7.6.2.tgz}
|
||||
engines: {node: '>=10'}
|
||||
@ -5840,6 +5915,11 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, tarball: https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swr@2.3.3:
|
||||
resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==, tarball: https://registry.npmjs.org/swr/-/swr-2.3.3.tgz}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, tarball: https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz}
|
||||
|
||||
@ -5877,6 +5957,10 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, tarball: https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==, tarball: https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-case@1.0.3:
|
||||
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==, tarball: https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz}
|
||||
|
||||
@ -6163,6 +6247,9 @@ packages:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, tarball: https://registry.npmjs.org/vary/-/vary-1.1.2.tgz}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==, tarball: https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz}
|
||||
|
||||
vfile-message@4.0.2:
|
||||
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==, tarball: https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz}
|
||||
|
||||
@ -6274,6 +6361,9 @@ packages:
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, tarball: https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==, tarball: https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz}
|
||||
engines: {node: '>=12'}
|
||||
@ -6405,6 +6495,11 @@ packages:
|
||||
yup@1.6.1:
|
||||
resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==, tarball: https://registry.npmjs.org/yup/-/yup-1.6.1.tgz}
|
||||
|
||||
zod-to-json-schema@3.24.5:
|
||||
resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz}
|
||||
peerDependencies:
|
||||
zod: ^3.24.1
|
||||
|
||||
zod-validation-error@3.4.0:
|
||||
resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==, tarball: https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@ -6424,6 +6519,45 @@ snapshots:
|
||||
|
||||
'@adobe/css-tools@4.4.1': {}
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.4(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.0
|
||||
nanoid: 3.3.8
|
||||
secure-json-parse: 2.7.0
|
||||
zod: 3.24.3
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.6(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.2
|
||||
nanoid: 3.3.8
|
||||
secure-json-parse: 2.7.0
|
||||
zod: 3.24.3
|
||||
|
||||
'@ai-sdk/provider@1.1.0':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/provider@1.1.2':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.2.6(react@18.3.1)(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.2.4(zod@3.24.3)
|
||||
'@ai-sdk/ui-utils': 1.2.5(zod@3.24.3)
|
||||
react: 18.3.1
|
||||
swr: 2.3.3(react@18.3.1)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 3.24.3
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.5(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.0
|
||||
'@ai-sdk/provider-utils': 2.2.4(zod@3.24.3)
|
||||
zod: 3.24.3
|
||||
zod-to-json-schema: 3.24.5(zod@3.24.3)
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@ -10183,8 +10317,39 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
devlop: 1.1.0
|
||||
hastscript: 9.0.1
|
||||
property-information: 7.0.0
|
||||
vfile: 6.0.3
|
||||
vfile-location: 5.0.3
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-parse-selector@2.2.5: {}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
hast-util-from-parse5: 8.0.3
|
||||
hast-util-to-parse5: 8.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
parse5: 7.1.2
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.2:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
@ -10205,6 +10370,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
devlop: 1.1.0
|
||||
property-information: 6.5.0
|
||||
space-separated-tokens: 2.0.2
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@ -10217,6 +10392,14 @@ snapshots:
|
||||
property-information: 5.6.0
|
||||
space-separated-tokens: 1.1.5
|
||||
|
||||
hastscript@9.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-parse-selector: 4.0.0
|
||||
property-information: 7.0.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
headers-polyfill@4.0.3: {}
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
@ -10235,6 +10418,8 @@ snapshots:
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
http-errors@2.0.0:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@ -10962,6 +11147,8 @@ snapshots:
|
||||
json-schema-traverse@0.4.1:
|
||||
optional: true
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
optional: true
|
||||
|
||||
@ -11986,6 +12173,8 @@ snapshots:
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
property-information@7.0.0: {}
|
||||
|
||||
protobufjs@7.4.0:
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
@ -12303,6 +12492,12 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
set-function-name: 2.0.1
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-raw: 9.1.0
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-gfm@4.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.3
|
||||
@ -12442,6 +12637,8 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
semver@7.6.2: {}
|
||||
|
||||
send@0.19.0:
|
||||
@ -12695,6 +12892,12 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swr@2.3.3(react@18.3.1):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.4.0(react@18.3.1)
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
@ -12753,6 +12956,8 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
tiny-case@1.0.3: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
@ -13043,6 +13248,11 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile: 6.0.3
|
||||
|
||||
vfile-message@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@ -13139,6 +13349,8 @@ snapshots:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webpack-sources@3.2.3: {}
|
||||
@ -13253,6 +13465,10 @@ snapshots:
|
||||
toposort: 2.0.2
|
||||
type-fest: 2.19.0
|
||||
|
||||
zod-to-json-schema@3.24.5(zod@3.24.3):
|
||||
dependencies:
|
||||
zod: 3.24.3
|
||||
|
||||
zod-validation-error@3.4.0(zod@3.24.3):
|
||||
dependencies:
|
||||
zod: 3.24.3
|
||||
|
@ -827,6 +827,13 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getDeploymentLLMs = async (): Promise<TypesGen.LanguageModelConfig> => {
|
||||
const response = await this.axios.get<TypesGen.LanguageModelConfig>(
|
||||
"/api/v2/deployment/llms",
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getOrganizationIdpSyncClaimFieldValues = async (
|
||||
organization: string,
|
||||
field: string,
|
||||
@ -2489,6 +2496,23 @@ class ApiMethods {
|
||||
markAllInboxNotificationsAsRead = async () => {
|
||||
await this.axios.put<void>("/api/v2/notifications/inbox/mark-all-as-read");
|
||||
};
|
||||
|
||||
createChat = async () => {
|
||||
const res = await this.axios.post<TypesGen.Chat>("/api/v2/chats");
|
||||
return res.data;
|
||||
};
|
||||
|
||||
getChats = async () => {
|
||||
const res = await this.axios.get<TypesGen.Chat[]>("/api/v2/chats");
|
||||
return res.data;
|
||||
};
|
||||
|
||||
getChatMessages = async (chatId: string) => {
|
||||
const res = await this.axios.get<TypesGen.ChatMessage[]>(
|
||||
`/api/v2/chats/${chatId}/messages`,
|
||||
);
|
||||
return res.data;
|
||||
};
|
||||
}
|
||||
|
||||
// This is a hard coded CSRF token/cookie pair for local development. In prod,
|
||||
|
25
site/src/api/queries/chats.ts
Normal file
25
site/src/api/queries/chats.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { API } from "api/api";
|
||||
import type { QueryClient } from "react-query";
|
||||
|
||||
export const createChat = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: API.createChat,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(["chats"]);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getChats = () => {
|
||||
return {
|
||||
queryKey: ["chats"],
|
||||
queryFn: API.getChats,
|
||||
};
|
||||
};
|
||||
|
||||
export const getChatMessages = (chatID: string) => {
|
||||
return {
|
||||
queryKey: ["chatMessages", chatID],
|
||||
queryFn: () => API.getChatMessages(chatID),
|
||||
};
|
||||
};
|
@ -36,3 +36,10 @@ export const deploymentIdpSyncFieldValues = (field: string) => {
|
||||
queryFn: () => API.getDeploymentIdpSyncFieldValues(field),
|
||||
};
|
||||
};
|
||||
|
||||
export const deploymentLanguageModels = () => {
|
||||
return {
|
||||
queryKey: ["deployment", "llms"],
|
||||
queryFn: API.getDeploymentLLMs,
|
||||
};
|
||||
};
|
||||
|
16
site/src/contexts/useAgenticChat.ts
Normal file
16
site/src/contexts/useAgenticChat.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { experiments } from "api/queries/experiments";
|
||||
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
interface AgenticChat {
|
||||
readonly enabled: boolean;
|
||||
}
|
||||
|
||||
export const useAgenticChat = (): AgenticChat => {
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const enabledExperimentsQuery = useQuery(experiments(metadata.experiments));
|
||||
return {
|
||||
enabled: enabledExperimentsQuery.data?.includes("agentic-chat") ?? false,
|
||||
};
|
||||
};
|
@ -4,6 +4,7 @@ import { Button } from "components/Button/Button";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { CoderIcon } from "components/Icons/CoderIcon";
|
||||
import type { ProxyContextValue } from "contexts/ProxyContext";
|
||||
import { useAgenticChat } from "contexts/useAgenticChat";
|
||||
import { useWebpushNotifications } from "contexts/useWebpushNotifications";
|
||||
import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox";
|
||||
import type { FC } from "react";
|
||||
@ -45,8 +46,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
canViewAuditLog,
|
||||
proxyContextValue,
|
||||
}) => {
|
||||
const { subscribed, enabled, loading, subscribe, unsubscribe } =
|
||||
useWebpushNotifications();
|
||||
const webPush = useWebpushNotifications();
|
||||
|
||||
return (
|
||||
<div className="border-0 border-b border-solid h-[72px] flex items-center leading-none px-6">
|
||||
@ -76,13 +76,21 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{enabled ? (
|
||||
subscribed ? (
|
||||
<Button variant="outline" disabled={loading} onClick={unsubscribe}>
|
||||
{webPush.enabled ? (
|
||||
webPush.subscribed ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={webPush.loading}
|
||||
onClick={webPush.unsubscribe}
|
||||
>
|
||||
Disable WebPush
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" disabled={loading} onClick={subscribe}>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={webPush.loading}
|
||||
onClick={webPush.subscribe}
|
||||
>
|
||||
Enable WebPush
|
||||
</Button>
|
||||
)
|
||||
@ -132,6 +140,7 @@ interface NavItemsProps {
|
||||
|
||||
const NavItems: FC<NavItemsProps> = ({ className }) => {
|
||||
const location = useLocation();
|
||||
const agenticChat = useAgenticChat();
|
||||
|
||||
return (
|
||||
<nav className={cn("flex items-center gap-4 h-full", className)}>
|
||||
@ -154,6 +163,16 @@ const NavItems: FC<NavItemsProps> = ({ className }) => {
|
||||
>
|
||||
Templates
|
||||
</NavLink>
|
||||
{agenticChat.enabled ? (
|
||||
<NavLink
|
||||
className={({ isActive }) => {
|
||||
return cn(linkStyles.default, isActive ? linkStyles.active : "");
|
||||
}}
|
||||
to="/chat"
|
||||
>
|
||||
Chat
|
||||
</NavLink>
|
||||
) : null}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
164
site/src/pages/ChatPage/ChatLanding.tsx
Normal file
164
site/src/pages/ChatPage/ChatLanding.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { createChat } from "api/queries/chats";
|
||||
import type { Chat } from "api/typesGenerated";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { type FC, type FormEvent, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LanguageModelSelector } from "./LanguageModelSelector";
|
||||
|
||||
export interface ChatLandingLocationState {
|
||||
chat: Chat;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const ChatLanding: FC = () => {
|
||||
const { user } = useAuthenticated();
|
||||
const theme = useTheme();
|
||||
const [input, setInput] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const createChatMutation = useMutation(createChat(queryClient));
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: theme.spacing(24),
|
||||
alignItems: "center",
|
||||
paddingBottom: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{/* Initial Welcome Message Area */}
|
||||
<div
|
||||
css={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(1),
|
||||
width: "100%",
|
||||
maxWidth: "700px",
|
||||
marginBottom: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
css={{
|
||||
fontSize: theme.typography.h4.fontSize,
|
||||
fontWeight: theme.typography.h4.fontWeight,
|
||||
lineHeight: theme.typography.h4.lineHeight,
|
||||
marginBottom: theme.spacing(1),
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Good evening, {user?.name.split(" ")[0]}
|
||||
</h1>
|
||||
<p
|
||||
css={{
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: theme.typography.h6.fontWeight,
|
||||
lineHeight: theme.typography.h6.lineHeight,
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
margin: 0,
|
||||
maxWidth: "500px",
|
||||
marginInline: "auto",
|
||||
}}
|
||||
>
|
||||
How can I help you today?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Input Form and Suggestions - Always Visible */}
|
||||
<div css={{ width: "100%", maxWidth: "700px", marginTop: "auto" }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setInput("Help me work on issue #...")}
|
||||
>
|
||||
Work on Issue
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setInput("Help me build a template for...")}
|
||||
>
|
||||
Build a Template
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setInput("Help me start a new project using...")}
|
||||
>
|
||||
Start a Project
|
||||
</Button>
|
||||
</Stack>
|
||||
<LanguageModelSelector />
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setInput("");
|
||||
const chat = await createChatMutation.mutateAsync();
|
||||
navigate(`/chat/${chat.id}`, {
|
||||
state: {
|
||||
chat,
|
||||
message: input,
|
||||
},
|
||||
});
|
||||
}}
|
||||
elevation={2}
|
||||
css={{
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: "12px",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
value={input}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(event.target.value);
|
||||
}}
|
||||
placeholder="Ask Coder..."
|
||||
required
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
multiline
|
||||
maxRows={5}
|
||||
css={{
|
||||
marginRight: theme.spacing(1),
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: "8px",
|
||||
padding: "10px 14px",
|
||||
},
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<IconButton type="submit" color="primary" disabled={!input.trim()}>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
</Margins>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatLanding;
|
246
site/src/pages/ChatPage/ChatLayout.tsx
Normal file
246
site/src/pages/ChatPage/ChatLayout.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import Button from "@mui/material/Button";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { createChat, getChats } from "api/queries/chats";
|
||||
import { deploymentLanguageModels } from "api/queries/deployment";
|
||||
import type { LanguageModelConfig } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { useAgenticChat } from "contexts/useAgenticChat";
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Link, Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export interface ChatContext {
|
||||
selectedModel: string;
|
||||
modelConfig: LanguageModelConfig;
|
||||
|
||||
setSelectedModel: (model: string) => void;
|
||||
}
|
||||
export const useChatContext = (): ChatContext => {
|
||||
const context = useContext(ChatContext);
|
||||
if (!context) {
|
||||
throw new Error("useChatContext must be used within a ChatProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ChatContext = createContext<ChatContext | undefined>(undefined);
|
||||
|
||||
const SELECTED_MODEL_KEY = "coder_chat_selected_model";
|
||||
|
||||
const ChatProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [selectedModel, setSelectedModel] = useState<string>(() => {
|
||||
const savedModel = localStorage.getItem(SELECTED_MODEL_KEY);
|
||||
return savedModel || "";
|
||||
});
|
||||
const modelConfigQuery = useQuery(deploymentLanguageModels());
|
||||
useEffect(() => {
|
||||
if (!modelConfigQuery.data) {
|
||||
return;
|
||||
}
|
||||
if (selectedModel === "") {
|
||||
const firstModel = modelConfigQuery.data.models[0]?.id; // Handle empty models array
|
||||
if (firstModel) {
|
||||
setSelectedModel(firstModel);
|
||||
localStorage.setItem(SELECTED_MODEL_KEY, firstModel);
|
||||
}
|
||||
}
|
||||
}, [modelConfigQuery.data, selectedModel]);
|
||||
|
||||
if (modelConfigQuery.error) {
|
||||
return <ErrorAlert error={modelConfigQuery.error} />;
|
||||
}
|
||||
|
||||
if (!modelConfigQuery.data) {
|
||||
return <Loader fullscreen />;
|
||||
}
|
||||
|
||||
const handleSetSelectedModel = (model: string) => {
|
||||
setSelectedModel(model);
|
||||
localStorage.setItem(SELECTED_MODEL_KEY, model);
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatContext.Provider
|
||||
value={{
|
||||
selectedModel,
|
||||
modelConfig: modelConfigQuery.data,
|
||||
setSelectedModel: handleSetSelectedModel,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChatLayout: FC = () => {
|
||||
const agenticChat = useAgenticChat();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: chats, isLoading: chatsLoading } = useQuery(getChats());
|
||||
const createChatMutation = useMutation(createChat(queryClient));
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { chatID } = useParams<{ chatID?: string }>();
|
||||
|
||||
const handleNewChat = () => {
|
||||
navigate("/chat");
|
||||
};
|
||||
|
||||
if (!agenticChat.enabled) {
|
||||
return (
|
||||
<Margins>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: "24px",
|
||||
alignItems: "center",
|
||||
paddingBottom: "16px",
|
||||
}}
|
||||
>
|
||||
<h1>Agentic Chat is not enabled</h1>
|
||||
<p>
|
||||
Agentic Chat is an experimental feature and is not enabled by
|
||||
default. Please contact your administrator for more information.
|
||||
</p>
|
||||
</div>
|
||||
</Margins>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// Outermost container: controls height and prevents page scroll
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
height: "calc(100vh - 164px)", // Assuming header height is 64px
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Sidebar Container (using Paper for background/border) */}
|
||||
<Paper
|
||||
elevation={1}
|
||||
square // Removes border-radius
|
||||
css={{
|
||||
width: 260,
|
||||
flexShrink: 0,
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%", // Take full height of the parent flex container
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
{/* Sidebar Header */}
|
||||
<div
|
||||
css={{
|
||||
padding: theme.spacing(1.5, 2),
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{/* Replaced Typography with div + styling */}
|
||||
<div
|
||||
css={{
|
||||
fontWeight: 600,
|
||||
fontSize: theme.typography.subtitle1.fontSize,
|
||||
lineHeight: theme.typography.subtitle1.lineHeight,
|
||||
}}
|
||||
>
|
||||
Chats
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon fontSize="small" />}
|
||||
onClick={handleNewChat}
|
||||
disabled={createChatMutation.isLoading}
|
||||
css={{
|
||||
lineHeight: 1.5,
|
||||
padding: theme.spacing(0.5, 1.5),
|
||||
}}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
{/* Sidebar Scrollable List Area */}
|
||||
<div css={{ overflowY: "auto", flexGrow: 1 }}>
|
||||
{chatsLoading ? (
|
||||
<Loader />
|
||||
) : chats && chats.length > 0 ? (
|
||||
<List dense>
|
||||
{chats.map((chat) => (
|
||||
<ListItem key={chat.id} disablePadding>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={`/chat/${chat.id}`}
|
||||
selected={chatID === chat.id}
|
||||
css={{
|
||||
padding: theme.spacing(1, 2),
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={chat.title || `Chat ${chat.id}`}
|
||||
primaryTypographyProps={{
|
||||
noWrap: true,
|
||||
variant: "body2",
|
||||
style: { overflow: "hidden", textOverflow: "ellipsis" },
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
// Replaced Typography with div + styling
|
||||
<div
|
||||
css={{
|
||||
padding: theme.spacing(2),
|
||||
textAlign: "center",
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
No chats yet. Start a new one!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
{/* Main Content Area Container */}
|
||||
<div
|
||||
css={{
|
||||
flexGrow: 1, // Takes remaining width
|
||||
height: "100%", // Takes full height of parent
|
||||
overflow: "hidden", // Prevents this container from scrolling
|
||||
display: "flex",
|
||||
flexDirection: "column", // Stacks ChatProvider/Outlet
|
||||
position: "relative", // Context for potential absolute children
|
||||
backgroundColor: theme.palette.background.default, // Ensure background consistency
|
||||
}}
|
||||
>
|
||||
<ChatProvider>
|
||||
{/* Outlet renders ChatMessages, which should have its own internal scroll */}
|
||||
<Outlet />
|
||||
</ChatProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
491
site/src/pages/ChatPage/ChatMessages.tsx
Normal file
491
site/src/pages/ChatPage/ChatMessages.tsx
Normal file
@ -0,0 +1,491 @@
|
||||
import { type Message, useChat } from "@ai-sdk/react";
|
||||
import { type Theme, keyframes, useTheme } from "@emotion/react";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { getChatMessages } from "api/queries/chats";
|
||||
import type { ChatMessage, CreateChatMessageRequest } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
type FC,
|
||||
type KeyboardEvent,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useQuery } from "react-query";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { ChatLandingLocationState } from "./ChatLanding";
|
||||
import { useChatContext } from "./ChatLayout";
|
||||
import { ChatToolInvocation } from "./ChatToolInvocation";
|
||||
import { LanguageModelSelector } from "./LanguageModelSelector";
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const renderReasoning = (reasoning: string, theme: Theme) => (
|
||||
<div
|
||||
css={{
|
||||
marginTop: theme.spacing(1),
|
||||
marginLeft: theme.spacing(2),
|
||||
borderLeft: `2px solid ${theme.palette.grey[400]}`,
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
fontStyle: "italic",
|
||||
color: theme.palette.text.secondary,
|
||||
animation: `${fadeIn} 0.3s ease-out`,
|
||||
fontSize: "0.875em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.grey[700],
|
||||
fontWeight: 500,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
💭 Reasoning:
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
whiteSpace: "pre-wrap",
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
padding: theme.spacing(1.5),
|
||||
borderRadius: "6px",
|
||||
fontSize: "0.95em",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{reasoning}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
const MessageBubble: FC<MessageBubbleProps> = memo(({ message }) => {
|
||||
const theme = useTheme();
|
||||
const isUser = message.role === "user";
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
justifyContent: isUser ? "flex-end" : "flex-start",
|
||||
maxWidth: "80%",
|
||||
marginLeft: isUser ? "auto" : 0,
|
||||
animation: `${fadeIn} 0.3s ease-out`,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={isUser ? 1 : 0}
|
||||
variant={isUser ? "elevation" : "outlined"}
|
||||
css={{
|
||||
padding: theme.spacing(1.25, 1.75),
|
||||
fontSize: "0.925rem",
|
||||
lineHeight: 1.5,
|
||||
backgroundColor: isUser
|
||||
? theme.palette.grey[900]
|
||||
: theme.palette.background.paper,
|
||||
borderColor: !isUser ? theme.palette.divider : undefined,
|
||||
color: isUser ? theme.palette.grey[50] : theme.palette.text.primary,
|
||||
borderRadius: "16px",
|
||||
borderBottomRightRadius: isUser ? "4px" : "16px",
|
||||
borderBottomLeftRadius: isUser ? "16px" : "4px",
|
||||
width: "auto",
|
||||
maxWidth: "100%",
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
maxHeight: "400px",
|
||||
height: "auto",
|
||||
borderRadius: "8px",
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
"& p": {
|
||||
margin: theme.spacing(1, 0),
|
||||
"&:first-of-type": {
|
||||
marginTop: 0,
|
||||
},
|
||||
"&:last-of-type": {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
"& ul, & ol": {
|
||||
margin: theme.spacing(1.5, 0),
|
||||
paddingLeft: theme.spacing(3),
|
||||
},
|
||||
"& li": {
|
||||
margin: theme.spacing(0.5, 0),
|
||||
},
|
||||
"& code:not(pre > code)": {
|
||||
backgroundColor: isUser
|
||||
? theme.palette.grey[700]
|
||||
: theme.palette.action.hover,
|
||||
color: isUser ? theme.palette.grey[50] : theme.palette.text.primary,
|
||||
padding: theme.spacing(0.25, 0.75),
|
||||
borderRadius: "4px",
|
||||
fontSize: "0.875em",
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
"& pre": {
|
||||
backgroundColor: isUser
|
||||
? theme.palette.common.black
|
||||
: theme.palette.grey[100],
|
||||
color: isUser
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.text.primary,
|
||||
padding: theme.spacing(1.5),
|
||||
borderRadius: "8px",
|
||||
overflowX: "auto",
|
||||
margin: theme.spacing(1.5, 0),
|
||||
width: "100%",
|
||||
"& code": {
|
||||
backgroundColor: "transparent",
|
||||
padding: 0,
|
||||
fontSize: "0.875em",
|
||||
fontFamily: "monospace",
|
||||
color: "inherit",
|
||||
},
|
||||
},
|
||||
"& a": {
|
||||
color: isUser
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.primary.main,
|
||||
textDecoration: "underline",
|
||||
fontWeight: 500,
|
||||
"&:hover": {
|
||||
textDecoration: "none",
|
||||
color: isUser
|
||||
? theme.palette.grey[300]
|
||||
: theme.palette.primary.dark,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{message.role === "assistant" && message.parts ? (
|
||||
<div>
|
||||
{message.parts.map((part) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={message.id}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
css={{
|
||||
"& pre": {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{part.text}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
case "tool-invocation":
|
||||
return (
|
||||
<div key={message.id}>
|
||||
<ChatToolInvocation
|
||||
toolInvocation={
|
||||
part.toolInvocation as ChatToolInvocation
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "reasoning":
|
||||
return (
|
||||
<div key={message.id}>
|
||||
{renderReasoning(part.reasoning, theme)}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface ChatViewProps {
|
||||
messages: Message[];
|
||||
input: string;
|
||||
handleInputChange: React.ChangeEventHandler<
|
||||
HTMLInputElement | HTMLTextAreaElement
|
||||
>;
|
||||
handleSubmit: (e?: React.FormEvent<HTMLFormElement>) => void;
|
||||
isLoading: boolean;
|
||||
chatID: string;
|
||||
}
|
||||
|
||||
const ChatView: FC<ChatViewProps> = ({
|
||||
messages,
|
||||
input,
|
||||
handleInputChange,
|
||||
handleSubmit,
|
||||
isLoading,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const chatContext = useChatContext();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
});
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
flexGrow: 1,
|
||||
overflowY: "auto",
|
||||
padding: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
maxWidth: "900px",
|
||||
width: "100%",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={`message-${message.id}`} message={message} />
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={{
|
||||
width: "100%",
|
||||
maxWidth: "900px",
|
||||
margin: "0 auto",
|
||||
padding: theme.spacing(2, 3, 2, 3),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
elevation={0}
|
||||
variant="outlined"
|
||||
css={{
|
||||
padding: theme.spacing(0.5, 0.5, 0.5, 1.5),
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
width: "100%",
|
||||
borderRadius: "12px",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
transition: "border-color 0.2s ease",
|
||||
"&:focus-within": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
marginRight: theme.spacing(1),
|
||||
alignSelf: "flex-end",
|
||||
marginBottom: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
<LanguageModelSelector />
|
||||
</div>
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
value={input}
|
||||
disabled={isLoading || chatContext.selectedModel === ""}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask Coder..."
|
||||
fullWidth
|
||||
variant="standard"
|
||||
multiline
|
||||
maxRows={5}
|
||||
InputProps={{ disableUnderline: true }}
|
||||
css={{
|
||||
alignSelf: "center",
|
||||
padding: theme.spacing(0.75, 0),
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<IconButton
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={
|
||||
!input.trim() || isLoading || chatContext.selectedModel === ""
|
||||
}
|
||||
css={{
|
||||
alignSelf: "flex-end",
|
||||
marginBottom: theme.spacing(0.5),
|
||||
transition: "transform 0.2s ease, background-color 0.2s ease",
|
||||
"&:not(:disabled):hover": {
|
||||
transform: "scale(1.1)",
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChatMessages: FC = () => {
|
||||
const { chatID } = useParams();
|
||||
if (!chatID) {
|
||||
throw new Error("Chat ID is required in URL path /chat/:chatID");
|
||||
}
|
||||
|
||||
const { state } = useLocation();
|
||||
const transferredState = state as ChatLandingLocationState | undefined;
|
||||
|
||||
const messagesQuery = useQuery<ChatMessage[], Error>(getChatMessages(chatID));
|
||||
|
||||
const chatContext = useChatContext();
|
||||
|
||||
const {
|
||||
messages,
|
||||
input,
|
||||
handleInputChange,
|
||||
handleSubmit: originalHandleSubmit,
|
||||
isLoading,
|
||||
setInput,
|
||||
setMessages,
|
||||
} = useChat({
|
||||
id: chatID,
|
||||
api: `/api/v2/chats/${chatID}/messages`,
|
||||
experimental_prepareRequestBody: (options): CreateChatMessageRequest => {
|
||||
const userMessages = options.messages.filter(
|
||||
(message) => message.role === "user",
|
||||
);
|
||||
const mostRecentUserMessage = userMessages.at(-1);
|
||||
return {
|
||||
model: chatContext.selectedModel,
|
||||
message: mostRecentUserMessage,
|
||||
thinking: false,
|
||||
};
|
||||
},
|
||||
initialInput: transferredState?.message,
|
||||
initialMessages: messagesQuery.data as Message[] | undefined,
|
||||
});
|
||||
|
||||
// Update messages from query data when it loads
|
||||
useEffect(() => {
|
||||
if (messagesQuery.data && messages.length === 0) {
|
||||
setMessages(messagesQuery.data as Message[]);
|
||||
}
|
||||
}, [messagesQuery.data, messages.length, setMessages]);
|
||||
|
||||
const handleSubmitCallback = useCallback(
|
||||
(e?: React.FormEvent<HTMLFormElement>) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!input.trim()) return;
|
||||
originalHandleSubmit();
|
||||
setInput(""); // Clear input after submit
|
||||
},
|
||||
[input, originalHandleSubmit, setInput],
|
||||
);
|
||||
|
||||
// Clear input and potentially submit on initial load with message
|
||||
useEffect(() => {
|
||||
if (transferredState?.message && input === transferredState.message) {
|
||||
// Prevent submitting if messages already exist (e.g., browser back/forward)
|
||||
if (messages.length === (messagesQuery.data?.length ?? 0)) {
|
||||
handleSubmitCallback(); // Use the correct callback name
|
||||
}
|
||||
// Clear the state to prevent re-submission on subsequent renders/navigation
|
||||
window.history.replaceState({}, document.title);
|
||||
}
|
||||
}, [
|
||||
transferredState?.message,
|
||||
input,
|
||||
handleSubmitCallback,
|
||||
messages.length,
|
||||
messagesQuery.data?.length,
|
||||
]); // Use the correct callback name
|
||||
|
||||
useEffect(() => {
|
||||
if (transferredState?.message) {
|
||||
// Logic potentially related to transferredState can go here if needed,
|
||||
}
|
||||
}, [transferredState?.message]);
|
||||
|
||||
if (messagesQuery.error) {
|
||||
return <ErrorAlert error={messagesQuery.error} />;
|
||||
}
|
||||
|
||||
if (messagesQuery.isLoading && messages.length === 0) {
|
||||
return <Loader fullscreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatView
|
||||
key={chatID}
|
||||
chatID={chatID}
|
||||
messages={messages}
|
||||
input={input}
|
||||
handleInputChange={handleInputChange}
|
||||
handleSubmit={handleSubmitCallback}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
1211
site/src/pages/ChatPage/ChatToolInvocation.stories.tsx
Normal file
1211
site/src/pages/ChatPage/ChatToolInvocation.stories.tsx
Normal file
File diff suppressed because it is too large
Load Diff
872
site/src/pages/ChatPage/ChatToolInvocation.tsx
Normal file
872
site/src/pages/ChatPage/ChatToolInvocation.tsx
Normal file
@ -0,0 +1,872 @@
|
||||
import type { ToolCall, ToolResult } from "@ai-sdk/provider-utils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import ArticleIcon from "@mui/icons-material/Article";
|
||||
import BuildIcon from "@mui/icons-material/Build";
|
||||
import CheckCircle from "@mui/icons-material/CheckCircle";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ErrorIcon from "@mui/icons-material/Error";
|
||||
import FileUploadIcon from "@mui/icons-material/FileUpload";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { type FC, memo, useMemo, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { TabLink, Tabs, TabsList } from "../../components/Tabs/Tabs";
|
||||
|
||||
interface ChatToolInvocationProps {
|
||||
toolInvocation: ChatToolInvocation;
|
||||
}
|
||||
|
||||
export const ChatToolInvocation: FC<ChatToolInvocationProps> = ({
|
||||
toolInvocation,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const friendlyName = useMemo(() => {
|
||||
return toolInvocation.toolName
|
||||
.replace("coder_", "")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}, [toolInvocation.toolName]);
|
||||
|
||||
const hasError = useMemo(() => {
|
||||
if (toolInvocation.state !== "result") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof toolInvocation.result === "object" &&
|
||||
toolInvocation.result !== null &&
|
||||
"error" in toolInvocation.result
|
||||
);
|
||||
}, [toolInvocation]);
|
||||
const statusColor = useMemo(() => {
|
||||
if (toolInvocation.state !== "result") {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
return hasError ? theme.palette.error.main : theme.palette.success.main;
|
||||
}, [toolInvocation, hasError, theme]);
|
||||
const tooltipContent = useMemo(() => {
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
style={dracula}
|
||||
css={{
|
||||
maxHeight: 300,
|
||||
overflow: "auto",
|
||||
fontSize: 14,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(1),
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "auto",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(toolInvocation, null, 2)}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
}, [toolInvocation, theme.shape.borderRadius, theme.spacing]);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(2),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(0.75),
|
||||
width: "fit-content",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{ display: "flex", alignItems: "center", gap: theme.spacing(1) }}
|
||||
>
|
||||
{toolInvocation.state !== "result" && (
|
||||
<CircularProgress
|
||||
size={16}
|
||||
css={{
|
||||
color: statusColor,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{toolInvocation.state === "result" ? (
|
||||
hasError ? (
|
||||
<ErrorIcon sx={{ color: statusColor, fontSize: 16 }} />
|
||||
) : (
|
||||
<CheckCircle sx={{ color: statusColor, fontSize: 16 }} />
|
||||
)
|
||||
) : null}
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.9rem",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{friendlyName}
|
||||
</div>
|
||||
<Tooltip title={tooltipContent}>
|
||||
<InfoIcon size={12} color={theme.palette.text.disabled} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{toolInvocation.state === "result" ? (
|
||||
<ChatToolInvocationResultPreview toolInvocation={toolInvocation} />
|
||||
) : (
|
||||
<ChatToolInvocationCallPreview toolInvocation={toolInvocation} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatToolInvocationCallPreview: FC<{
|
||||
toolInvocation: Extract<
|
||||
ChatToolInvocation,
|
||||
{ state: "call" | "partial-call" }
|
||||
>;
|
||||
}> = memo(({ toolInvocation }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let content: React.ReactNode;
|
||||
switch (toolInvocation.toolName) {
|
||||
case "coder_upload_tar_file":
|
||||
content = (
|
||||
<FilePreview
|
||||
files={toolInvocation.args?.files || {}}
|
||||
prefix="Uploading files:"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div css={{ paddingLeft: theme.spacing(3) }}>{content}</div>;
|
||||
});
|
||||
|
||||
const ChatToolInvocationResultPreview: FC<{
|
||||
toolInvocation: Extract<ChatToolInvocation, { state: "result" }>;
|
||||
}> = memo(({ toolInvocation }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!toolInvocation.result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof toolInvocation.result === "object" &&
|
||||
"error" in toolInvocation.result
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let content: React.ReactNode;
|
||||
switch (toolInvocation.toolName) {
|
||||
case "coder_get_workspace":
|
||||
case "coder_create_workspace":
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
{toolInvocation.result.template_icon && (
|
||||
<img
|
||||
src={toolInvocation.result.template_icon || "/icon/code.svg"}
|
||||
alt={toolInvocation.result.template_display_name || "Template"}
|
||||
css={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{toolInvocation.result.name}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{toolInvocation.result.template_display_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "coder_list_workspaces":
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
{toolInvocation.result.map((workspace) => (
|
||||
<div
|
||||
key={workspace.id}
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
{workspace.template_icon && (
|
||||
<img
|
||||
src={workspace.template_icon || "/icon/code.svg"}
|
||||
alt={workspace.template_display_name || "Template"}
|
||||
css={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{workspace.name}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{workspace.template_display_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "coder_list_templates": {
|
||||
const templates = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
{templates.map((template) => (
|
||||
<div
|
||||
key={template.id}
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
<CodeIcon sx={{ width: 32, height: 32 }} />
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{template.name}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: 200,
|
||||
}}
|
||||
title={template.description}
|
||||
>
|
||||
{template.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{templates.length === 0 && <div>No templates found.</div>}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_template_version_parameters": {
|
||||
const params = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<SettingsIcon fontSize="small" />
|
||||
{params.length > 0
|
||||
? `${params.length} parameter(s)`
|
||||
: "No parameters"}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_get_authenticated_user": {
|
||||
const user = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
<Avatar src={user.avatar_url}>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{user.username}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_create_workspace_build": {
|
||||
const build = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<BuildIcon fontSize="small" />
|
||||
Build #{build.build_number} ({build.transition}) status:{" "}
|
||||
{build.status}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_create_template_version": {
|
||||
const version = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
}}
|
||||
>
|
||||
<CodeIcon fontSize="small" />
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>{version.name}</div>
|
||||
{version.message && (
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{version.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_get_workspace_agent_logs":
|
||||
case "coder_get_workspace_build_logs":
|
||||
case "coder_get_template_version_logs": {
|
||||
const logs = toolInvocation.result;
|
||||
const totalLines = logs.length;
|
||||
const maxLinesToShow = 5;
|
||||
const lastLogs = logs.slice(-maxLinesToShow);
|
||||
const hiddenLines = totalLines - lastLogs.length;
|
||||
|
||||
const totalLinesText = `${totalLines} log line${totalLines !== 1 ? "s" : ""}`;
|
||||
const hiddenLinesText =
|
||||
hiddenLines > 0
|
||||
? `... hiding ${hiddenLines} more line${hiddenLines !== 1 ? "s" : ""} ...`
|
||||
: null;
|
||||
|
||||
const logsToShow = hiddenLinesText
|
||||
? [hiddenLinesText, ...lastLogs]
|
||||
: lastLogs;
|
||||
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<ArticleIcon fontSize="small" />
|
||||
Retrieved {totalLinesText}.
|
||||
</div>
|
||||
{logsToShow.length > 0 && (
|
||||
<SyntaxHighlighter
|
||||
language="log"
|
||||
style={dracula}
|
||||
customStyle={{
|
||||
fontSize: "0.8rem",
|
||||
padding: theme.spacing(1),
|
||||
margin: 0,
|
||||
maxHeight: 150,
|
||||
overflowY: "auto",
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "auto",
|
||||
}}
|
||||
showLineNumbers={false}
|
||||
lineNumberStyle={{ display: "none" }}
|
||||
>
|
||||
{logsToShow.join("\n")}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_update_template_active_version":
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<SettingsIcon fontSize="small" />
|
||||
{toolInvocation.result}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "coder_upload_tar_file":
|
||||
content = (
|
||||
<FilePreview files={toolInvocation.args.files} prefix={"Uploaded!"} />
|
||||
);
|
||||
break;
|
||||
case "coder_create_template": {
|
||||
const template = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={template.icon || "/icon/code.svg"}
|
||||
alt={template.display_name || "Template"}
|
||||
css={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{template.name}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{template.display_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_delete_template":
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
{toolInvocation.result}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "coder_get_template_version": {
|
||||
const version = toolInvocation.result;
|
||||
content = (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
}}
|
||||
>
|
||||
<CodeIcon fontSize="small" />
|
||||
<div>
|
||||
<div css={{ fontWeight: 500, lineHeight: 1.4 }}>{version.name}</div>
|
||||
{version.message && (
|
||||
<div
|
||||
css={{
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{version.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "coder_download_tar_file": {
|
||||
const files = toolInvocation.result;
|
||||
content = <FilePreview files={files} prefix="Files:" />;
|
||||
break;
|
||||
}
|
||||
// Add default case or handle other tools if necessary
|
||||
}
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
paddingLeft: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// New component to preview files with tabs
|
||||
const FilePreview: FC<{ files: Record<string, string>; prefix?: string }> =
|
||||
memo(({ files, prefix }) => {
|
||||
const theme = useTheme();
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
const fileEntries = useMemo(() => Object.entries(files), [files]);
|
||||
|
||||
if (fileEntries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
const getLanguage = (filename: string): string => {
|
||||
if (filename.includes("Dockerfile")) {
|
||||
return "dockerfile";
|
||||
}
|
||||
const extension = filename.split(".").pop()?.toLowerCase();
|
||||
switch (extension) {
|
||||
case "tf":
|
||||
return "hcl";
|
||||
case "json":
|
||||
return "json";
|
||||
case "yaml":
|
||||
case "yml":
|
||||
return "yaml";
|
||||
case "js":
|
||||
case "jsx":
|
||||
return "javascript";
|
||||
case "ts":
|
||||
case "tsx":
|
||||
return "typescript";
|
||||
case "py":
|
||||
return "python";
|
||||
case "go":
|
||||
return "go";
|
||||
case "rb":
|
||||
return "ruby";
|
||||
case "java":
|
||||
return "java";
|
||||
case "sh":
|
||||
return "bash";
|
||||
case "md":
|
||||
return "markdown";
|
||||
default:
|
||||
return "plaintext";
|
||||
}
|
||||
};
|
||||
|
||||
// Get filename and content based on the selectedTab index
|
||||
const [selectedFilename, selectedContent] = fileEntries[selectedTab] ?? [
|
||||
"",
|
||||
"",
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
{prefix && (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
fontSize: "0.875rem",
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<FileUploadIcon fontSize="small" />
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
{/* Use custom Tabs component with active prop */}
|
||||
<Tabs active={selectedFilename} className="flex-shrink-0">
|
||||
<TabsList>
|
||||
{fileEntries.map(([filename], index) => (
|
||||
<TabLink
|
||||
key={filename}
|
||||
value={filename} // This matches the 'active' prop on Tabs
|
||||
to="" // Dummy link, not navigating
|
||||
css={{ whiteSpace: "nowrap" }} // Prevent wrapping
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent any potential default link behavior
|
||||
handleTabChange(index);
|
||||
}}
|
||||
>
|
||||
{filename}
|
||||
</TabLink>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<SyntaxHighlighter
|
||||
language={getLanguage(selectedFilename)}
|
||||
style={vscDarkPlus}
|
||||
customStyle={{
|
||||
fontSize: "0.8rem",
|
||||
padding: theme.spacing(1),
|
||||
margin: 0,
|
||||
maxHeight: 200,
|
||||
overflowY: "auto",
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "auto",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
showLineNumbers={false}
|
||||
lineNumberStyle={{ display: "none" }}
|
||||
>
|
||||
{selectedContent}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: generate these from codersdk/toolsdk.go.
|
||||
export type ChatToolInvocation =
|
||||
| ToolInvocation<
|
||||
"coder_get_workspace",
|
||||
{
|
||||
workspace_id: string;
|
||||
},
|
||||
TypesGen.Workspace
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_create_workspace",
|
||||
{
|
||||
user: string;
|
||||
template_version_id: string;
|
||||
name: string;
|
||||
rich_parameters: Record<string, string>;
|
||||
},
|
||||
TypesGen.Workspace
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_list_workspaces",
|
||||
{
|
||||
owner: string;
|
||||
},
|
||||
Pick<
|
||||
TypesGen.Workspace,
|
||||
| "id"
|
||||
| "name"
|
||||
| "template_id"
|
||||
| "template_name"
|
||||
| "template_display_name"
|
||||
| "template_icon"
|
||||
| "template_active_version_id"
|
||||
| "outdated"
|
||||
>[]
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_list_templates",
|
||||
Record<string, never>,
|
||||
Pick<
|
||||
TypesGen.Template,
|
||||
| "id"
|
||||
| "name"
|
||||
| "description"
|
||||
| "active_version_id"
|
||||
| "active_user_count"
|
||||
>[]
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_template_version_parameters",
|
||||
{
|
||||
template_version_id: string;
|
||||
},
|
||||
TypesGen.TemplateVersionParameter[]
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_get_authenticated_user",
|
||||
Record<string, never>,
|
||||
TypesGen.User
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_create_workspace_build",
|
||||
{
|
||||
workspace_id: string;
|
||||
template_version_id?: string;
|
||||
transition: "start" | "stop" | "delete";
|
||||
},
|
||||
TypesGen.WorkspaceBuild
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_create_template_version",
|
||||
{
|
||||
template_id?: string;
|
||||
file_id: string;
|
||||
},
|
||||
TypesGen.TemplateVersion
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_get_workspace_agent_logs",
|
||||
{
|
||||
workspace_agent_id: string;
|
||||
},
|
||||
string[]
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_get_workspace_build_logs",
|
||||
{
|
||||
workspace_build_id: string;
|
||||
},
|
||||
string[]
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_get_template_version_logs",
|
||||
{
|
||||
template_version_id: string;
|
||||
},
|
||||
string[]
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_get_template_version",
|
||||
{
|
||||
template_version_id: string;
|
||||
},
|
||||
TypesGen.TemplateVersion
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_download_tar_file",
|
||||
{
|
||||
file_id: string;
|
||||
},
|
||||
Record<string, string>
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_update_template_active_version",
|
||||
{
|
||||
template_id: string;
|
||||
template_version_id: string;
|
||||
},
|
||||
string
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_upload_tar_file",
|
||||
{
|
||||
files: Record<string, string>;
|
||||
},
|
||||
TypesGen.UploadResponse
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_create_template",
|
||||
{
|
||||
name: string;
|
||||
},
|
||||
TypesGen.Template
|
||||
>
|
||||
| ToolInvocation<
|
||||
"coder_delete_template",
|
||||
{
|
||||
template_id: string;
|
||||
},
|
||||
string
|
||||
>;
|
||||
|
||||
type ToolInvocation<N extends string, A, R> =
|
||||
| ({
|
||||
state: "partial-call";
|
||||
step?: number;
|
||||
} & ToolCall<N, A>)
|
||||
| ({
|
||||
state: "call";
|
||||
step?: number;
|
||||
} & ToolCall<N, A>)
|
||||
| ({
|
||||
state: "result";
|
||||
step?: number;
|
||||
} & ToolResult<
|
||||
N,
|
||||
A,
|
||||
| R
|
||||
| {
|
||||
error: string;
|
||||
}
|
||||
>);
|
73
site/src/pages/ChatPage/LanguageModelSelector.tsx
Normal file
73
site/src/pages/ChatPage/LanguageModelSelector.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select from "@mui/material/Select";
|
||||
import { deploymentLanguageModels } from "api/queries/deployment";
|
||||
import type { LanguageModel } from "api/typesGenerated"; // Assuming types live here based on project structure
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useChatContext } from "./ChatLayout";
|
||||
|
||||
export const LanguageModelSelector: FC = () => {
|
||||
const theme = useTheme();
|
||||
const { setSelectedModel, modelConfig, selectedModel } = useChatContext();
|
||||
const {
|
||||
data: languageModelConfig,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery(deploymentLanguageModels());
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader size={14} />;
|
||||
}
|
||||
|
||||
if (error || !languageModelConfig) {
|
||||
console.error("Failed to load language models:", error);
|
||||
return (
|
||||
<div css={{ color: theme.palette.error.main }}>Error loading models.</div>
|
||||
);
|
||||
}
|
||||
|
||||
const models = Array.from(languageModelConfig.models).toSorted((a, b) => {
|
||||
// Sort by provider first, then by display name
|
||||
const compareProvider = a.provider.localeCompare(b.provider);
|
||||
if (compareProvider !== 0) {
|
||||
return compareProvider;
|
||||
}
|
||||
return a.display_name.localeCompare(b.display_name);
|
||||
});
|
||||
|
||||
if (models.length === 0) {
|
||||
return (
|
||||
<div css={{ color: theme.palette.text.disabled }}>
|
||||
No language models available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="model-select-label">Model</InputLabel>
|
||||
<Select
|
||||
labelId="model-select-label"
|
||||
value={selectedModel}
|
||||
label="Model"
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
disabled={isLoading || models.length === 0}
|
||||
>
|
||||
{!selectedModel && (
|
||||
<MenuItem value="" disabled>
|
||||
Select a model...
|
||||
</MenuItem>
|
||||
)}
|
||||
{models.map((model: LanguageModel) => (
|
||||
<MenuItem key={model.id} value={model.id}>
|
||||
{model.display_name} ({model.provider})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
import { GlobalErrorBoundary } from "components/ErrorBoundary/GlobalErrorBoundary";
|
||||
import { ChatLayout } from "pages/ChatPage/ChatLayout";
|
||||
import { ChatMessages } from "pages/ChatPage/ChatMessages";
|
||||
import { TemplateRedirectController } from "pages/TemplatePage/TemplateRedirectController";
|
||||
import { Suspense, lazy } from "react";
|
||||
import {
|
||||
@ -31,6 +33,7 @@ const NotFoundPage = lazy(() => import("./pages/404Page/404Page"));
|
||||
const DeploymentSettingsLayout = lazy(
|
||||
() => import("./modules/management/DeploymentSettingsLayout"),
|
||||
);
|
||||
const ChatLanding = lazy(() => import("./pages/ChatPage/ChatLanding"));
|
||||
const DeploymentConfigProvider = lazy(
|
||||
() => import("./modules/management/DeploymentConfigProvider"),
|
||||
);
|
||||
@ -422,6 +425,11 @@ export const router = createBrowserRouter(
|
||||
|
||||
<Route path="/audit" element={<AuditPage />} />
|
||||
|
||||
<Route path="/chat" element={<ChatLayout />}>
|
||||
<Route index element={<ChatLanding />} />
|
||||
<Route path=":chatID" element={<ChatMessages />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/organizations" element={<OrganizationSettingsLayout />}>
|
||||
<Route path="new" element={<CreateOrganizationPage />} />
|
||||
|
||||
|
Reference in New Issue
Block a user