diff --git a/web/packages/teleport/src/Assist/Assist.tsx b/web/packages/teleport/src/Assist/Assist.tsx index 96695f5447a74..ebacae54eb444 100644 --- a/web/packages/teleport/src/Assist/Assist.tsx +++ b/web/packages/teleport/src/Assist/Assist.tsx @@ -22,10 +22,11 @@ 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'; +import { ConversationTitle } from 'teleport/Assist/ConversationTitle'; +import { LandingPage } from 'teleport/Assist/LandingPage'; +import { Chat } from 'teleport/Assist/Chat'; +import { Sidebar } from 'teleport/Assist/Sidebar'; const Container = styled.div` display: flex; @@ -55,10 +56,14 @@ export function Assist() { ) : ( - + )} {createPortal(, document.getElementById('assist-sidebar'))} + {createPortal( + , + document.getElementById('topbar-portal') + )} ); } diff --git a/web/packages/teleport/src/Assist/Chat/Chat.tsx b/web/packages/teleport/src/Assist/Chat/Chat.tsx index 76772582c0fee..e64c3b0f1c36c 100644 --- a/web/packages/teleport/src/Assist/Chat/Chat.tsx +++ b/web/packages/teleport/src/Assist/Chat/Chat.tsx @@ -45,7 +45,6 @@ import { import { ChatBox } from './ChatBox'; import { ChatItem } from './ChatItem'; -import { ExampleChatItem } from './ChatItem/ChatItem'; const Container = styled.div` flex: 1; @@ -210,15 +209,3 @@ export function Chat(props: ChatProps) { ); } - -export function NewChat() { - return ( - - - - - - - - ); -} diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/ChatItem.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/ChatItem.tsx index 7475f09577789..5aab8eeada780 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/ChatItem.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/ChatItem.tsx @@ -28,8 +28,6 @@ import { useTeleport } from 'teleport'; import { getBorderRadius } from 'teleport/Assist/Chat/ChatItem/utils'; -import { ExampleList } from '../Examples/ExampleList'; - import { Author, Message, Type } from '../../services/messages'; import { Timestamp } from '../Timestamp'; @@ -178,6 +176,12 @@ const Output = styled.div` monospace; `; +const ErrorMessage = styled.div` + color: ${p => p.theme.colors.error.main}; + font-size: 15px; + font-weight: 500; +`; + marked.setOptions({ renderer: new marked.Renderer(), highlight: function (code, lang) { @@ -245,7 +249,11 @@ export function ChatItem(props: ChatItemProps) { Command ran on node{' '} {props.message.content.nodeId} - {props.message.content.payload} + {props.message.content.errorMsg ? ( + {props.message.content.errorMsg} + ) : ( + {props.message.content.payload} + )} @@ -292,28 +300,3 @@ export function ChatItem(props: ChatItemProps) { ); } - -export function ExampleChatItem() { - const ctx = useTeleport(); - - return ( - - - Hey {ctx.storeUser.state.username}, I'm Teleport - a powerful tool that - can assist you in managing your Teleport cluster via OpenAI GPT-4. -
-
- Start a new chat with me on the left to get started! Here's some of the - things I can do: - -
- - - - - - Teleport - -
- ); -} diff --git a/web/packages/teleport/src/Assist/Chat/Examples/ExampleList.tsx b/web/packages/teleport/src/Assist/Chat/Examples/ExampleList.tsx deleted file mode 100644 index 2ded03cc7badb..0000000000000 --- a/web/packages/teleport/src/Assist/Chat/Examples/ExampleList.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* -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 { - RemoteCommandIcon, - SearchIcon, - ServerIcon, - UpgradeIcon, -} from 'design/SVGIcon'; - -import { ExampleItem } from './ExampleItem'; - -const Container = styled.div` - display: flex; - flex-wrap: wrap; - margin-top: 10px; - margin-bottom: 30px; - - > * { - margin-top: 10px; - } -`; - -export function ExampleList() { - return ( - - - - Connect to a server - - - - Analyze audit logs - - - Run remote commands - - - - Upgrade your nodes - - - ); -} diff --git a/web/packages/teleport/src/Assist/Chat/Examples/ExampleItem.tsx b/web/packages/teleport/src/Assist/ConversationTitle.tsx similarity index 52% rename from web/packages/teleport/src/Assist/Chat/Examples/ExampleItem.tsx rename to web/packages/teleport/src/Assist/ConversationTitle.tsx index fb02abcc97be1..bffda9b5feac0 100644 --- a/web/packages/teleport/src/Assist/Chat/Examples/ExampleItem.tsx +++ b/web/packages/teleport/src/Assist/ConversationTitle.tsx @@ -14,33 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import styled, { keyframes } from 'styled-components'; +import React from 'react'; +import { useParams } from 'react-router'; -const appear = keyframes` - 0% { - opacity: 0; - } +import { useConversations } from 'teleport/Assist/contexts/conversations'; - 100% { - opacity: 1; - } -`; - -export const ExampleItem = styled.div` - border: 1px solid ${p => p.theme.colors.text.main}; - margin-right: 20px; - padding: 10px 15px; - border-radius: 5px; - display: flex; - align-items: center; - font-size: 14px; - opacity: 0; - animation: ${appear} linear 0.6s forwards; - - svg { - margin-right: 15px; - path { - fill: ${p => p.theme.colors.text.main}; +export function ConversationTitle() { + const { conversations } = useConversations(); + const params = useParams<{ conversationId: string }>(); + + if (params.conversationId) { + const conversation = conversations.find( + conversation => conversation.id === params.conversationId + ); + + if (conversation) { + return <> - {conversation.title}; } } -`; + + return null; +} diff --git a/web/packages/teleport/src/Assist/LandingPage.tsx b/web/packages/teleport/src/Assist/LandingPage.tsx new file mode 100644 index 0000000000000..7d07d130044c6 --- /dev/null +++ b/web/packages/teleport/src/Assist/LandingPage.tsx @@ -0,0 +1,189 @@ +/* +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 } from 'react'; +import styled, { useTheme } from 'styled-components'; + +import { + AuditLogIcon, + PlusIcon, + RemoteCommandIcon, + SearchIcon, + ServerIcon, +} from 'design/SVGIcon'; + +import { useHistory } from 'react-router'; + +import Flex from 'design/Flex'; + +import Link from 'design/Link'; + +import cfg from 'teleport/config'; +import { useConversations } from 'teleport/Assist/contexts/conversations'; + +const Container = styled.div` + display: flex; + flex: 1; + justify-content: center; +`; + +const Content = styled.div` + background: ${p => p.theme.colors.levels.popout}; + color: ${p => p.theme.colors.text.main}; + font-size: 15px; + padding: 20px 25px; + border-radius: 7px; + margin-top: 50px; + width: 700px; +`; + +const Title = styled.h2` + margin: 0 0 20px; +`; + +const SubTitle = styled.h3` + margin: 0 0 10px; +`; + +const Features = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 20px; +`; + +const Feature = styled.div` + background: ${p => p.theme.colors.spotBackground[0]}; + margin-bottom: 10px; + margin-right: 20px; + padding: 10px 15px; + border-radius: 5px; + display: flex; + align-items: center; + font-size: 15px; + + svg { + margin-right: 15px; + + path { + fill: ${p => p.theme.colors.text.main}; + } + } +`; + +const Warning = styled.div` + border: 2px solid ${p => p.theme.colors.warning.main}; + border-radius: 7px; + padding: 10px 15px; + margin-bottom: 15px; +`; + +const NewChatButton = styled.div` + padding: 10px 20px; + border-radius: 7px; + font-size: 15px; + font-weight: bold; + display: flex; + cursor: pointer; + margin: 0 15px; + background: ${p => p.theme.colors.buttons.primary.default}; + color: ${p => p.theme.colors.buttons.primary.text}; + align-items: center; + justify-content: center; + + svg { + position: relative; + margin-right: 10px; + } + + &:hover { + background: ${p => p.theme.colors.buttons.primary.hover}; + } +`; + +export function LandingPage() { + const theme = useTheme(); + + const history = useHistory(); + + const { create } = useConversations(); + + const handleNewChat = useCallback(() => { + create().then(conversationId => + history.push(cfg.getAssistConversationUrl(conversationId)) + ); + }, []); + + return ( + + + Teleport Assist + +

+ Teleport Assist utilizes facts about your infrastructure to help + answer questions, generate command line scripts and help you perform + routine tasks on target resources. +

+ + + This is an experimental project. The AI can hallucinate and produce + harmful commands. Do not use in production. Let us know what you think + in our{' '} + + community Slack. + + + + Features + + + + Connect to your servers + + + Run commands across multiple nodes + + + + Coming Soon + + + + + Analyze the audit log + + + + Interpret command outputs + + + & much more! + + + + + handleNewChat()}> + + Start a new conversation + + +
+
+ ); +} diff --git a/web/packages/teleport/src/Assist/Sidebar/index.ts b/web/packages/teleport/src/Assist/Sidebar/index.ts index d522624c38911..813fb0ef8bc9e 100644 --- a/web/packages/teleport/src/Assist/Sidebar/index.ts +++ b/web/packages/teleport/src/Assist/Sidebar/index.ts @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -export { Sidebar as default } from './Sidebar'; +export { Sidebar } from './Sidebar'; diff --git a/web/packages/teleport/src/Assist/contexts/conversations.tsx b/web/packages/teleport/src/Assist/contexts/conversations.tsx index 1f5da5dbf5ed2..50046cd22fc04 100644 --- a/web/packages/teleport/src/Assist/contexts/conversations.tsx +++ b/web/packages/teleport/src/Assist/contexts/conversations.tsx @@ -23,7 +23,7 @@ import React, { useState, } from 'react'; -import logger from 'shared/libs/logger'; +import Logger from 'shared/libs/logger'; import api from 'teleport/services/api'; import cfg from 'teleport/config'; @@ -53,6 +53,8 @@ interface ListConversationsResponse { ]; } +const logger = Logger.create('assist'); + const ConversationsContext = createContext({ conversations: [], create: () => Promise.resolve(void 0), diff --git a/web/packages/teleport/src/Assist/contexts/messages.tsx b/web/packages/teleport/src/Assist/contexts/messages.tsx index ad99de260e029..4b1b05cc1fb59 100644 --- a/web/packages/teleport/src/Assist/contexts/messages.tsx +++ b/web/packages/teleport/src/Assist/contexts/messages.tsx @@ -26,7 +26,7 @@ import useWebSocket from 'react-use-websocket'; import { useParams } from 'react-router'; -import logger from 'shared/libs/logger'; +import Logger from 'shared/libs/logger'; import api, { getAccessToken, getHostName } from 'teleport/services/api'; @@ -222,12 +222,18 @@ async function convertServerMessage( }); // The offset here is set base on A/B test that was run between me, myself and I. - const resp = await api - .fetch(sessionUrl + '/stream?offset=0&bytes=4096', { - Accept: 'text/plain', - 'Content-Type': 'text/plain; charset=utf-8', - }) - .then(response => response.text()); + const resp = await api.fetch(sessionUrl + '/stream?offset=0&bytes=4096', { + Accept: 'text/plain', + 'Content-Type': 'text/plain; charset=utf-8', + }); + + let msg; + let errorMsg; + if (resp.status === 200) { + msg = await resp.text(); + } else { + errorMsg = 'No session recording. The command execution failed.'; + } const newMessage: Message = { author: Author.Teleport, @@ -236,7 +242,8 @@ async function convertServerMessage( type: Type.ExecuteCommandOutput, nodeId: payload.node_id, executionId: payload.execution_id, - payload: resp, + payload: msg, + errorMsg, }, }; @@ -319,6 +326,8 @@ export async function setConversationTitle( }); } +const logger = Logger.create('assist'); + export function MessagesContextProvider( props: PropsWithChildren ) { diff --git a/web/packages/teleport/src/Assist/services/messages.ts b/web/packages/teleport/src/Assist/services/messages.ts index 398d016d39ac2..c896e5c0f79de 100644 --- a/web/packages/teleport/src/Assist/services/messages.ts +++ b/web/packages/teleport/src/Assist/services/messages.ts @@ -57,6 +57,7 @@ export interface CommandExecutionOutput { nodeId: string; executionId: string; payload: string; + errorMsg?: string; } export type MessageContent = diff --git a/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx b/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx index d8fceb9c26636..828f92f8cfc0d 100644 --- a/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx +++ b/web/packages/teleport/src/Navigation/NavigationSwitcher.tsx @@ -295,8 +295,8 @@ export function NavigationSwitcher(props: NavigationSwitcherProps) { New! {' '} - Connect Teleport to OpenAI GPT-4 and try out our new Assist - integration + Try out Teleport Assist, a GPT-4-powered AI assistant that leverages + your infrastructure diff --git a/web/packages/teleport/src/TopBar/TopBar.tsx b/web/packages/teleport/src/TopBar/TopBar.tsx index 86f28b3fb34ec..15214065bc672 100644 --- a/web/packages/teleport/src/TopBar/TopBar.tsx +++ b/web/packages/teleport/src/TopBar/TopBar.tsx @@ -72,6 +72,8 @@ export function TopBar() { {!hasClusterUrl && ( {title} + + )} + {shownBanners.map(banner => (