Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component will be deleted entirely when the migration is complete. Right now, this renders for every route. When every route has been successfully migrated, this won't be imported by any route anymore and can be deleted.

* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { useLocation, useParams } from 'react-router-dom';

import { EuiCode, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';

export const RouteDisplay: React.FC = () => {
const location = useLocation();
const params = useParams();
const { euiTheme } = useEuiTheme();

const containerStyles = css`
padding: ${euiTheme.size.xl};
height: 100%;
`;

const codeStyles = css`
font-size: ${euiTheme.size.l};
padding: ${euiTheme.size.m};
`;

return (
<EuiFlexGroup
direction="column"
alignItems="center"
justifyContent="center"
css={containerStyles}
gutterSize="l"
>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>Current Route</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCode css={codeStyles}>{location.pathname}</EuiCode>
</EuiFlexItem>
{Object.keys(params).length > 0 && (
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<strong>Route params:</strong> {JSON.stringify(params)}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';

import { UnifiedSidebar } from './unified_sidebar/unified_sidebar';

interface AppLayoutProps {
children: React.ReactNode;
}

export const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
const { euiTheme } = useEuiTheme();

const contentStyles = css`
overflow: auto;
background-color: ${euiTheme.colors.backgroundBasePlain};
`;

return (
<EuiFlexGroup gutterSize="none" style={{ height: '100%', width: '100%' }}>
<EuiFlexItem grow={false}>
<UnifiedSidebar />
</EuiFlexItem>
<EuiFlexItem css={contentStyles}>{children}</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';

import { EuiFlexItem, EuiText, EuiSelect, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import useLocalStorage from 'react-use/lib/useLocalStorage';

import { useAgentBuilderAgents } from '../../../hooks/agents/use_agents';
import { storageKeys } from '../../../storage_keys';

const labels = {
agentLabel: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.agentLabel', {
defaultMessage: 'Agent',
}),
selectAgent: i18n.translate('xpack.agentBuilder.sidebar.agentSelector.selectAgent', {
defaultMessage: 'Select agent',
}),
};

interface AgentSelectorProps {
agentId: string;
getNavigationPath: (newAgentId: string) => string;
}

export const AgentSelector: React.FC<AgentSelectorProps> = ({ agentId, getNavigationPath }) => {
const { agents, isLoading } = useAgentBuilderAgents();
const navigate = useNavigate();
const [, setStoredAgentId] = useLocalStorage<string>(storageKeys.agentId);

const handleAgentChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const newAgentId = e.target.value;
setStoredAgentId(newAgentId);
navigate(getNavigationPath(newAgentId));
},
[navigate, setStoredAgentId, getNavigationPath]
);

const agentOptions = agents.map((agent) => ({
value: agent.id,
text: agent.name,
}));

return (
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<strong>{labels.agentLabel}</strong>
</EuiText>
{isLoading ? (
<EuiLoadingSpinner size="s" />
) : (
<EuiSelect
compressed
options={agentOptions}
value={agentId}
onChange={handleAgentChange}
aria-label={labels.selectAgent}
/>
)}
</EuiFlexItem>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have 1 unified sidebar so the content can be changed inside of it with a nice transition to be defined by design.

* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import useLocalStorage from 'react-use/lib/useLocalStorage';

import { EuiPanel } from '@elastic/eui';
import { css } from '@emotion/react';

import { storageKeys } from '../../../storage_keys';
import { getSidebarViewForRoute, getAgentIdFromPath } from '../../../route_config';
import { ConversationSidebarView } from './views/conversation_view';
import { AgentSettingsSidebarView } from './views/agent_settings_view';
import { ManageSidebarView } from './views/manage_view';

const SIDEBAR_WIDTH = 200;

export const UnifiedSidebar: React.FC = () => {
const location = useLocation();
const sidebarView = getSidebarViewForRoute(location.pathname);
const agentIdFromPath = getAgentIdFromPath(location.pathname);
const [, setStoredAgentId] = useLocalStorage<string>(storageKeys.agentId);

useEffect(() => {
if (agentIdFromPath) {
setStoredAgentId(agentIdFromPath);
}
}, [agentIdFromPath, setStoredAgentId]);

const sidebarStyles = css`
width: ${SIDEBAR_WIDTH}px;
min-width: ${SIDEBAR_WIDTH}px;
height: 100%;
border-radius: 0;
`;

return (
<EuiPanel
css={sidebarStyles}
paddingSize="m"
hasShadow={false}
hasBorder
role="navigation"
aria-label="Agent Builder navigation"
>
{sidebarView === 'conversation' && <ConversationSidebarView pathname={location.pathname} />}
{sidebarView === 'agentSettings' && <AgentSettingsSidebarView pathname={location.pathname} />}
{sidebarView === 'manage' && <ManageSidebarView pathname={location.pathname} />}
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useCallback, useMemo } from 'react';
import { Link } from 'react-router-dom-v5-compat';

import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';

import { appPaths } from '../../../../utils/app_paths';
import { getAgentIdFromPath, getAgentSettingsNavItems } from '../../../../route_config';
import { AgentSelector } from '../agent_selector';

const labels = {
back: i18n.translate('xpack.agentBuilder.sidebar.agentSettings.back', {
defaultMessage: '← Back',
}),
title: i18n.translate('xpack.agentBuilder.sidebar.agentSettings.title', {
defaultMessage: 'Agent Settings',
}),
};

interface AgentSettingsSidebarViewProps {
pathname: string;
}

export const AgentSettingsSidebarView: React.FC<AgentSettingsSidebarViewProps> = ({ pathname }) => {
const agentId = getAgentIdFromPath(pathname) ?? 'elastic-ai-agent';
const { euiTheme } = useEuiTheme();

const linkStyles = css`
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
`;

const activeLinkStyles = css`
${linkStyles}
font-weight: ${euiTheme.font.weight.bold};
color: ${euiTheme.colors.primaryText};
`;

const getNavigationPath = useCallback(
(newAgentId: string) => pathname.replace(`/agents/${agentId}`, `/agents/${newAgentId}`),
[pathname, agentId]
);

const navItems = useMemo(() => {
return getAgentSettingsNavItems(agentId);
}, [agentId]);

const isActive = (path: string) => pathname === path;

return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<Link to={appPaths.agent.root({ agentId })} css={linkStyles}>
<EuiText size="s">{labels.back}</EuiText>
</Link>
</EuiFlexItem>

<EuiHorizontalRule margin="s" />

<AgentSelector agentId={agentId} getNavigationPath={getNavigationPath} />

<EuiHorizontalRule margin="s" />

<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<strong>{labels.title}</strong>
</EuiText>
</EuiFlexItem>

{navItems.map((item) => (
<EuiFlexItem grow={false} key={item.path}>
<Link to={item.path} css={isActive(item.path) ? activeLinkStyles : linkStyles}>
<EuiText size="s">{item.label}</EuiText>
</Link>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useCallback } from 'react';
import { Link } from 'react-router-dom-v5-compat';

import { EuiFlexGroup, EuiFlexItem, EuiText, EuiHorizontalRule } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';

import { appPaths } from '../../../../utils/app_paths';
import { getAgentIdFromPath } from '../../../../route_config';
import { AgentSelector } from '../agent_selector';

const labels = {
customize: i18n.translate('xpack.agentBuilder.sidebar.conversation.customize', {
defaultMessage: 'Customize',
}),
conversationsTitle: i18n.translate('xpack.agentBuilder.sidebar.conversation.conversationsTitle', {
defaultMessage: 'Conversations',
}),
newConversation: i18n.translate('xpack.agentBuilder.sidebar.conversation.newConversation', {
defaultMessage: '+ New conversation',
}),
manageComponents: i18n.translate('xpack.agentBuilder.sidebar.conversation.manageComponents', {
defaultMessage: 'Manage components',
}),
};

interface ConversationSidebarViewProps {
pathname: string;
}

export const ConversationSidebarView: React.FC<ConversationSidebarViewProps> = ({ pathname }) => {
const agentId = getAgentIdFromPath(pathname) ?? 'elastic-ai-agent';

const linkStyles = css`
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
`;

const getNavigationPath = useCallback(
(newAgentId: string) => appPaths.agent.root({ agentId: newAgentId }),
[]
);

return (
<EuiFlexGroup direction="column" gutterSize="s">
<AgentSelector agentId={agentId} getNavigationPath={getNavigationPath} />

<EuiFlexItem grow={false}>
<Link to={appPaths.agent.instructions({ agentId })} css={linkStyles}>
<EuiText size="s">{labels.customize}</EuiText>
</Link>
</EuiFlexItem>

<EuiHorizontalRule margin="s" />

<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
<strong>{labels.conversationsTitle}</strong>
</EuiText>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<Link to={appPaths.agent.root({ agentId })} css={linkStyles}>
<EuiText size="s">{labels.newConversation}</EuiText>
</Link>
</EuiFlexItem>

<EuiHorizontalRule margin="s" />

<EuiFlexItem grow={false}>
<Link to={appPaths.manage.agents} css={linkStyles}>
<EuiText size="s">{labels.manageComponents}</EuiText>
</Link>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Loading
Loading