From 4e3734fc4cb37beacc11d218672c963fb8bc3fd0 Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Thu, 11 May 2023 14:56:53 -0400 Subject: [PATCH] Backport Assist UI --- web/packages/build/vite/config.ts | 17 + web/packages/design/src/SVGIcon/Chat.tsx | 29 + web/packages/design/src/SVGIcon/ChatGPT.tsx | 29 + web/packages/design/src/SVGIcon/Edit.tsx | 29 + web/packages/design/src/SVGIcon/Label.tsx | 33 + web/packages/design/src/SVGIcon/Plus.tsx | 29 + .../design/src/SVGIcon/RemoteCommand.tsx | 29 + web/packages/design/src/SVGIcon/Run.tsx | 30 + web/packages/design/src/SVGIcon/Search.tsx | 34 + web/packages/design/src/SVGIcon/Server.tsx | 34 + .../design/src/SVGIcon/ServerIcon.tsx | 29 + web/packages/design/src/SVGIcon/Upgrade.tsx | 29 + web/packages/design/src/SVGIcon/User.tsx | 29 + web/packages/design/src/SVGIcon/index.ts | 13 +- .../design/src/ThemeProvider/globals.js | 14 +- .../design/src/assets/images/icons/openai.svg | 1 + .../src/assets/images/icons/teleport.png | Bin 0 -> 4442 bytes web/packages/shared/hooks/useLocalStorage.ts | 58 + web/packages/shared/package.json | 8 + web/packages/teleport/src/Assist/Assist.tsx | 64 + .../teleport/src/Assist/Chat/Avatar.ts | 46 + .../teleport/src/Assist/Chat/Chat.tsx | 224 ++++ .../src/Assist/Chat/ChatBox/ChatBox.tsx | 119 ++ .../teleport/src/Assist/Chat/ChatBox/index.ts | 17 + .../Assist/Chat/ChatItem/Action/Action.tsx | 343 ++++++ .../Chat/ChatItem/Action/ActionForm.tsx | 250 ++++ .../Assist/Chat/ChatItem/Action/Actions.tsx | 196 +++ .../Chat/ChatItem/Action/ExecResult.tsx | 116 ++ .../Assist/Chat/ChatItem/Action/RunAction.tsx | 205 ++++ .../src/Assist/Chat/ChatItem/Action/common.ts | 55 + .../src/Assist/Chat/ChatItem/Action/index.ts | 18 + .../src/Assist/Chat/ChatItem/Action/types.ts | 41 + .../src/Assist/Chat/ChatItem/ChatItem.tsx | 318 +++++ .../src/Assist/Chat/ChatItem/index.ts | 17 + .../src/Assist/Chat/ChatItem/styles/code.ts | 92 ++ .../Assist/Chat/ChatItem/styles/markdown.ts | 1064 +++++++++++++++++ .../src/Assist/Chat/ChatItem/utils.ts | 27 + .../src/Assist/Chat/Examples/ExampleItem.tsx | 46 + .../src/Assist/Chat/Examples/ExampleList.tsx | 60 + .../teleport/src/Assist/Chat/Timestamp.tsx | 72 ++ .../teleport/src/Assist/Chat/Typing.ts | 49 + .../teleport/src/Assist/Chat/index.ts | 17 + web/packages/teleport/src/Assist/Dots.tsx | 79 ++ .../teleport/src/Assist/Sidebar/Sidebar.tsx | 154 +++ .../teleport/src/Assist/Sidebar/index.ts | 17 + .../src/Assist/contexts/conversations.tsx | 129 ++ .../teleport/src/Assist/contexts/messages.tsx | 417 +++++++ web/packages/teleport/src/Assist/index.ts | 17 + .../teleport/src/Assist/services/messages.ts | 78 ++ .../src/Integrations/IntegrationList.tsx | 5 + .../teleport/src/Integrations/fixtures.ts | 8 + .../teleport/src/Navigation/AssistTooltip.ts | 92 ++ .../teleport/src/Navigation/Navigation.tsx | 1 - .../NavigationCategoryContainer.tsx | 10 + .../src/Navigation/NavigationSwitcher.tsx | 88 +- .../teleport/src/Navigation/categories.ts | 2 + web/packages/teleport/src/config.ts | 67 ++ web/packages/teleport/src/features.tsx | 17 + web/packages/teleport/src/mocks/contexts.ts | 1 + .../src/services/integrations/types.ts | 2 +- .../src/services/localStorage/localStorage.ts | 19 +- .../src/services/localStorage/types.ts | 1 + .../teleport/src/services/ping/makePing.ts | 3 +- .../teleport/src/services/ping/ping.test.ts | 2 +- .../teleport/src/services/ping/types.ts | 1 + .../teleport/src/services/user/makeAcl.ts | 2 + .../teleport/src/services/user/types.ts | 1 + .../teleport/src/services/user/user.test.ts | 7 + .../teleport/src/stores/storeUserContext.ts | 4 + web/packages/teleport/src/teleportContext.tsx | 7 +- web/packages/teleport/src/types.ts | 1 + yarn.lock | 37 + 72 files changed, 5178 insertions(+), 21 deletions(-) create mode 100644 web/packages/design/src/SVGIcon/Chat.tsx create mode 100644 web/packages/design/src/SVGIcon/ChatGPT.tsx create mode 100644 web/packages/design/src/SVGIcon/Edit.tsx create mode 100644 web/packages/design/src/SVGIcon/Label.tsx create mode 100644 web/packages/design/src/SVGIcon/Plus.tsx create mode 100644 web/packages/design/src/SVGIcon/RemoteCommand.tsx create mode 100644 web/packages/design/src/SVGIcon/Run.tsx create mode 100644 web/packages/design/src/SVGIcon/Search.tsx create mode 100644 web/packages/design/src/SVGIcon/Server.tsx create mode 100644 web/packages/design/src/SVGIcon/ServerIcon.tsx create mode 100644 web/packages/design/src/SVGIcon/Upgrade.tsx create mode 100644 web/packages/design/src/SVGIcon/User.tsx create mode 100644 web/packages/design/src/assets/images/icons/openai.svg create mode 100644 web/packages/design/src/assets/images/icons/teleport.png create mode 100644 web/packages/shared/hooks/useLocalStorage.ts create mode 100644 web/packages/teleport/src/Assist/Assist.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/Avatar.ts create mode 100644 web/packages/teleport/src/Assist/Chat/Chat.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatBox/ChatBox.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatBox/index.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/Action.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/ActionForm.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/Actions.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/ExecResult.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/common.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/index.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/Action/types.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/ChatItem.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/index.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/styles/code.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/styles/markdown.ts create mode 100644 web/packages/teleport/src/Assist/Chat/ChatItem/utils.ts create mode 100644 web/packages/teleport/src/Assist/Chat/Examples/ExampleItem.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/Examples/ExampleList.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/Timestamp.tsx create mode 100644 web/packages/teleport/src/Assist/Chat/Typing.ts create mode 100644 web/packages/teleport/src/Assist/Chat/index.ts create mode 100644 web/packages/teleport/src/Assist/Dots.tsx create mode 100644 web/packages/teleport/src/Assist/Sidebar/Sidebar.tsx create mode 100644 web/packages/teleport/src/Assist/Sidebar/index.ts create mode 100644 web/packages/teleport/src/Assist/contexts/conversations.tsx create mode 100644 web/packages/teleport/src/Assist/contexts/messages.tsx create mode 100644 web/packages/teleport/src/Assist/index.ts create mode 100644 web/packages/teleport/src/Assist/services/messages.ts create mode 100644 web/packages/teleport/src/Navigation/AssistTooltip.ts diff --git a/web/packages/build/vite/config.ts b/web/packages/build/vite/config.ts index 8e508589d3d92..79733501c40b4 100644 --- a/web/packages/build/vite/config.ts +++ b/web/packages/build/vite/config.ts @@ -95,6 +95,23 @@ export function createViteConfig( secure: false, ws: true, }, + '^\\/v1\\/webapi\\/assistant\\/(.*?)': { + target: `https://${target}`, + changeOrigin: false, + secure: false, + }, + '^\\/v1\\/webapi\\/sites\\/(.*?)\\/assistant': { + target: `wss://${target}`, + changeOrigin: false, + secure: false, + ws: true, + }, + '^\\/v1\\/webapi\\/command\\/(.*?)/execute': { + target: `wss://${target}`, + changeOrigin: false, + secure: false, + ws: true, + }, '/web/config.js': { target: `https://${target}`, changeOrigin: true, diff --git a/web/packages/design/src/SVGIcon/Chat.tsx b/web/packages/design/src/SVGIcon/Chat.tsx new file mode 100644 index 0000000000000..8246adc8e8d40 --- /dev/null +++ b/web/packages/design/src/SVGIcon/Chat.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function ChatIcon({ size = 22, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/ChatGPT.tsx b/web/packages/design/src/SVGIcon/ChatGPT.tsx new file mode 100644 index 0000000000000..15b5a8e127a7b --- /dev/null +++ b/web/packages/design/src/SVGIcon/ChatGPT.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from 'design/SVGIcon/SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function ChatGPTIcon({ size = 20, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Edit.tsx b/web/packages/design/src/SVGIcon/Edit.tsx new file mode 100644 index 0000000000000..2a75b03b48393 --- /dev/null +++ b/web/packages/design/src/SVGIcon/Edit.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function EditIcon({ size = 24, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Label.tsx b/web/packages/design/src/SVGIcon/Label.tsx new file mode 100644 index 0000000000000..eaf647bfa7a0e --- /dev/null +++ b/web/packages/design/src/SVGIcon/Label.tsx @@ -0,0 +1,33 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import type { SVGIconProps } from './common'; + +export function LabelIcon({ size = 14, fill = 'white' }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Plus.tsx b/web/packages/design/src/SVGIcon/Plus.tsx new file mode 100644 index 0000000000000..f5073384cdc22 --- /dev/null +++ b/web/packages/design/src/SVGIcon/Plus.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function PlusIcon({ size = 30, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/RemoteCommand.tsx b/web/packages/design/src/SVGIcon/RemoteCommand.tsx new file mode 100644 index 0000000000000..dad6234fafa88 --- /dev/null +++ b/web/packages/design/src/SVGIcon/RemoteCommand.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function RemoteCommandIcon({ size = 24, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Run.tsx b/web/packages/design/src/SVGIcon/Run.tsx new file mode 100644 index 0000000000000..6869dfe693bf6 --- /dev/null +++ b/web/packages/design/src/SVGIcon/Run.tsx @@ -0,0 +1,30 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function RunIcon({ size = 48, fill = 'white' }: SVGIconProps) { + return ( + + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Search.tsx b/web/packages/design/src/SVGIcon/Search.tsx new file mode 100644 index 0000000000000..096bb44927eaf --- /dev/null +++ b/web/packages/design/src/SVGIcon/Search.tsx @@ -0,0 +1,34 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function SearchIcon({ size = 24, fill }: SVGIconProps) { + return ( + + + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Server.tsx b/web/packages/design/src/SVGIcon/Server.tsx new file mode 100644 index 0000000000000..968611a8fe198 --- /dev/null +++ b/web/packages/design/src/SVGIcon/Server.tsx @@ -0,0 +1,34 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import type { SVGIconProps } from './common'; + +export function ServerIcon({ size = 13, fill = 'white' }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/ServerIcon.tsx b/web/packages/design/src/SVGIcon/ServerIcon.tsx new file mode 100644 index 0000000000000..cb3f2bd439129 --- /dev/null +++ b/web/packages/design/src/SVGIcon/ServerIcon.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function ServerIcon({ size = 48, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/Upgrade.tsx b/web/packages/design/src/SVGIcon/Upgrade.tsx new file mode 100644 index 0000000000000..fb921ec2d6d4c --- /dev/null +++ b/web/packages/design/src/SVGIcon/Upgrade.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from './SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function UpgradeIcon({ size = 50, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/User.tsx b/web/packages/design/src/SVGIcon/User.tsx new file mode 100644 index 0000000000000..51ef1f3c78e10 --- /dev/null +++ b/web/packages/design/src/SVGIcon/User.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { SVGIcon } from 'design/SVGIcon/SVGIcon'; + +import type { SVGIconProps } from './common'; + +export function UserIcon({ size = 14, fill }: SVGIconProps) { + return ( + + + + ); +} diff --git a/web/packages/design/src/SVGIcon/index.ts b/web/packages/design/src/SVGIcon/index.ts index 82d788b83b1ab..07d0bbf855875 100644 --- a/web/packages/design/src/SVGIcon/index.ts +++ b/web/packages/design/src/SVGIcon/index.ts @@ -21,21 +21,32 @@ export { ApplicationsIcon } from './Applications'; export { AuditLogIcon } from './AuditLog'; export { AuthConnectorsIcon } from './AuthConnectors'; export { AWSIcon } from './AWS'; -export { LockIcon } from './Lock'; +export { ChatGPTIcon } from './ChatGPT'; +export { ChatIcon } from './Chat'; export { ChevronRightIcon } from './ChevronRight'; export { DatabasesIcon } from './Databases'; export { DevicesIcon } from './Devices'; export { DesktopsIcon } from './Desktops'; export { DownloadsIcon } from './Downloads'; +export { EditIcon } from './Edit'; export { ExternalLinkIcon } from './ExternalLink'; export { IntegrationsIcon } from './Integrations'; export { KubernetesIcon } from './Kubernetes'; +export { LabelIcon } from './Label'; +export { LockIcon } from './Lock'; export { LogoutIcon } from './Logout'; export { ManageClustersIcon } from './ManageClusters'; +export { PlusIcon } from './Plus'; +export { RemoteCommandIcon } from './RemoteCommand'; export { RolesIcon } from './Roles'; +export { RunIcon } from './Run'; +export { SearchIcon } from './Search'; +export { ServerIcon } from './Server'; export { ServersIcon } from './Servers'; export { SessionRecordingsIcon } from './SessionRecordings'; export { SupportIcon } from './Support'; export { TrustedClustersIcon } from './TrustedClusters'; +export { UpgradeIcon } from './Upgrade'; +export { UserIcon } from './User'; export { UsersIcon } from './Users'; export { UserSettingsIcon } from './UserSettings'; diff --git a/web/packages/design/src/ThemeProvider/globals.js b/web/packages/design/src/ThemeProvider/globals.js index d8e000ccdcd51..aed44550dcc12 100644 --- a/web/packages/design/src/ThemeProvider/globals.js +++ b/web/packages/design/src/ThemeProvider/globals.js @@ -35,20 +35,26 @@ const GlobalStyle = createGlobalStyle` font-family: ${props => props.theme.font}; } - // custom scrollbars - ::-webkit-scrollbar { + // custom scrollbars with the ability to use the default scrollbar behavior via adding the attribute [data-scrollbar=default] + :not([data-scrollbar="default"])::-webkit-scrollbar { width: 8px; height: 8px; } - ::-webkit-scrollbar-thumb { + :not([data-scrollbar="default"])::-webkit-scrollbar-thumb { background: #757575; } - ::-webkit-scrollbar-corner { + :not([data-scrollbar="default"])::-webkit-scrollbar-corner { background: rgba(0,0,0,0.5); } + :root { + color-scheme: ${props => + props.theme + .name}; // this ensures Chrome's scrollbars are set to the right color depending on the theme + } + // remove dotted Firefox outline button, a { outline: 0; diff --git a/web/packages/design/src/assets/images/icons/openai.svg b/web/packages/design/src/assets/images/icons/openai.svg new file mode 100644 index 0000000000000..e04db75a5bbdc --- /dev/null +++ b/web/packages/design/src/assets/images/icons/openai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/packages/design/src/assets/images/icons/teleport.png b/web/packages/design/src/assets/images/icons/teleport.png new file mode 100644 index 0000000000000000000000000000000000000000..29dcc133a1f75fcee5329e442cf814293e5b3fb4 GIT binary patch literal 4442 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 literal 0 HcmV?d00001 diff --git a/web/packages/shared/hooks/useLocalStorage.ts b/web/packages/shared/hooks/useLocalStorage.ts new file mode 100644 index 0000000000000..4ca4d80e14947 --- /dev/null +++ b/web/packages/shared/hooks/useLocalStorage.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Gravitational, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; + +export function useLocalStorage( + key: string, + initialValue: T +): [T, Dispatch>] { + const read = useCallback(() => { + const value = window.localStorage.getItem(key); + + if (!value) { + return initialValue; + } + + try { + return JSON.parse(value) as T; + } catch (err) { + return initialValue; + } + }, [initialValue, key]); + + const [storedValue, setStoredValue] = useState(read()); + + const write = useCallback((value: SetStateAction) => { + const newValue = value instanceof Function ? value(storedValue) : value; + + window.localStorage.setItem(key, JSON.stringify(newValue)); + + setStoredValue(newValue); + }, []); + + useEffect(() => { + setStoredValue(read()); + }, []); + + return [storedValue, write]; +} diff --git a/web/packages/shared/package.json b/web/packages/shared/package.json index 5492f31a1d738..bfe098b4fc964 100644 --- a/web/packages/shared/package.json +++ b/web/packages/shared/package.json @@ -14,15 +14,23 @@ "create-react-class": "^15.6.3", "cross-env": "5.0.5", "date-fns": "^2.28.0", + "dompurify": "^3.0.1", + "highlight.js": "^11.7.0", "highlight-words-core": "^1.2.2", + "marked": "^4.3.0", "react": "^16.8.4", "react-day-picker": "7.3.2", "react-dom": "^16.8.4", "react-router": "5.1.1", "react-router-dom": "5.1.1", "react-select": "^3.0.8", + "react-use-websocket": "^4.3.1", "tslib": "^2.4.0", "whatwg-fetch": "^3.0.0", "@gravitational/design": "1.0.0" + }, + "devDependencies": { + "@types/dompurify": "^3.0.0", + "@types/marked": "^4.0.8" } } diff --git a/web/packages/teleport/src/Assist/Assist.tsx b/web/packages/teleport/src/Assist/Assist.tsx new file mode 100644 index 0000000000000..96695f5447a74 --- /dev/null +++ b/web/packages/teleport/src/Assist/Assist.tsx @@ -0,0 +1,64 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import styled from 'styled-components'; + +import { createPortal } from 'react-dom'; + +import { useParams } from 'react-router'; + +import { MessagesContextProvider } from 'teleport/Assist/contexts/messages'; +import { Chat } from 'teleport/Assist/Chat'; +import { ConversationsContextProvider } from 'teleport/Assist/contexts/conversations'; +import { NewChat } from 'teleport/Assist/Chat/Chat'; +import Sidebar from 'teleport/Assist/Sidebar'; + +const Container = styled.div` + display: flex; +`; + +const ChatContainer = styled.div` + display: flex; + max-width: 1600px; + height: calc(100vh - 72px); + width: 100%; +`; + +export function Assist() { + const params = useParams<{ conversationId: string }>(); + + return ( + + {params.conversationId ? ( + + + + + + + + ) : ( + + )} + + {createPortal(, document.getElementById('assist-sidebar'))} + + ); +} diff --git a/web/packages/teleport/src/Assist/Chat/Avatar.ts b/web/packages/teleport/src/Assist/Chat/Avatar.ts new file mode 100644 index 0000000000000..d7c8fb99e7aa1 --- /dev/null +++ b/web/packages/teleport/src/Assist/Chat/Avatar.ts @@ -0,0 +1,46 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import styled from 'styled-components'; + +export const AvatarContainer = styled.div` + display: flex; + align-items: center; + color: rgba(0, 0, 0, 0.5); + + strong { + display: block; + margin-right: 10px; + color: rgba(0, 0, 0, 0.9); + } +`; + +export const ChatItemAvatarImage = styled.div<{ backgroundImage: string }>` + background: url(${p => p.backgroundImage}) no-repeat; + width: 22px; + height: 22px; + overflow: hidden; + background-size: cover; +`; + +export const ChatItemAvatarTeleport = styled.div` + background: ${props => props.theme.colors.brand}; + padding: 4px; + border-radius: 10px; + left: 0; + right: auto; + margin-right: 10px; +`; diff --git a/web/packages/teleport/src/Assist/Chat/Chat.tsx b/web/packages/teleport/src/Assist/Chat/Chat.tsx new file mode 100644 index 0000000000000..76772582c0fee --- /dev/null +++ b/web/packages/teleport/src/Assist/Chat/Chat.tsx @@ -0,0 +1,224 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import teleport from 'design/assets/images/icons/teleport.png'; + +import logger from 'shared/libs/logger'; + +import { Dots } from 'teleport/Assist/Dots'; + +import { + Typing, + TypingContainer, + TypingDot, +} from 'teleport/Assist/Chat/Typing'; + +import { + AvatarContainer, + ChatItemAvatarImage, + ChatItemAvatarTeleport, +} from 'teleport/Assist/Chat/Avatar'; + +import { useConversations } from 'teleport/Assist/contexts/conversations'; + +import { + generateTitle, + setConversationTitle, + useMessages, +} from '../contexts/messages'; + +import { ChatBox } from './ChatBox'; +import { ChatItem } from './ChatItem'; +import { ExampleChatItem } from './ChatItem/ChatItem'; + +const Container = styled.div` + flex: 1; + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; +`; + +const Content = styled.div.attrs({ 'data-scrollbar': 'default' })` + flex: 1 1 auto; + overflow-y: auto; + padding-top: 30px; + display: flex; + justify-content: center; +`; + +const Padding = styled.div` + padding: 30px; + box-sizing: border-box; +`; + +const LoadingContainer = styled.div` + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +`; + +const Width = styled.div` + max-width: 1200px; + width: 100%; +`; + +class ChatProps { + conversationId: string; +} + +export function Chat(props: ChatProps) { + const ref = useRef(null); + + const [error, setError] = useState(null); + const { + send, + messages, + loading, + responding, + error: messagesError, + } = useMessages(); + const { conversations, setConversations } = useConversations(); + + const scrollTextarea = useCallback(() => { + ref.current?.scrollIntoView({ behavior: 'smooth' }); + }, [ref.current]); + + useEffect(() => { + scrollTextarea(); + }, [messages, scrollTextarea]); + + const handleSubmit = useCallback( + (message: string) => { + send(message).then(() => { + if (messages.length == 1) { + // Use the second message/first message from a user to generate the title. + (async () => { + try { + // Generate title using the last message and OpenAI API. + const title = await generateTitle(message); + // Set the title in the backend. + await setConversationTitle(props.conversationId, title); + // Update the title in the frontend. + setConversations(conversations => + conversations.map(c => { + if (c.id === props.conversationId) { + c.title = title; + } + return c; + }) + ); + } catch (err) { + setError('An error occurred when setting the conversation title'); + + logger.error(err); + } + })(); + } + }); + }, + [messages, conversations, setConversations] + ); + + const items = messages.map((message, index) => ( + + )); + + let content; + if (loading) { + content = ( + + + + ); + } else { + content = ( + + {items} + + {responding && ( + + + + + + + + + + + + + + )} + +
+ + ); + } + + return ( + + + {content} + + +
+ + + +
+
+ ); +} + +export function NewChat() { + return ( + + + + + + + + ); +} diff --git a/web/packages/teleport/src/Assist/Chat/ChatBox/ChatBox.tsx b/web/packages/teleport/src/Assist/Chat/ChatBox/ChatBox.tsx new file mode 100644 index 0000000000000..4c4c3e97ee41b --- /dev/null +++ b/web/packages/teleport/src/Assist/Chat/ChatBox/ChatBox.tsx @@ -0,0 +1,119 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { + ChangeEvent, + KeyboardEvent, + useEffect, + useRef, + useState, +} from 'react'; +import styled from 'styled-components'; + +import { useMessages } from 'teleport/Assist/contexts/messages'; + +interface ChatBoxProps { + disabled?: boolean; + onSubmit: (value: string) => void; + errorMessage: string | null; +} + +const Container = styled.div` + padding: 0 30px 30px; +`; + +const TextArea = styled.textarea` + width: 100%; + background: ${props => props.theme.colors.levels.popout}; + color: ${props => props.theme.colors.text.main}; + border: 2px solid ${props => props.theme.colors.spotBackground[1]}; + border-radius: 10px; + resize: none; + padding: 20px 20px 5px 30px; + font-size: 16px; + line-height: 24px; + box-sizing: border-box; + + &:focus { + outline: none; + border-color: ${props => props.theme.colors.spotBackground[2]}; + } + + ::placeholder { + color: ${props => props.theme.colors.text.muted}; + } +`; + +const ErrorMessage = styled.div` + color: ${p => p.theme.colors.error.main}; + font-weight: 700; + margin-bottom: 5px; +`; + +export function ChatBox(props: ChatBoxProps) { + const [value, setValue] = useState(''); + const ref = useRef(null); + + const { responding } = useMessages(); + + useEffect(() => { + if (ref.current) { + ref.current.style.height = '0px'; + const scrollHeight = ref.current.scrollHeight; + + ref.current.style.height = `${scrollHeight + 20}px`; + } + }, [ref.current, value]); + + useEffect(() => { + if (ref.current) { + ref.current.focus(); + } + }, [props.disabled]); + + function handleChange(event: ChangeEvent) { + setValue(event.target.value); + } + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + event.stopPropagation(); + + if (!responding && value) { + props.onSubmit(value); + setValue(''); + } + } + } + + return ( + + {props.errorMessage && {props.errorMessage}} + +