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.errorMsg ? (
+ {props.message.content.errorMsg}
+ ) : (
+
+ )}
@@ -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 => (