mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
feat: workchat assistant list/details/edit page (#217984)
## Summary ### UI changes - assistant UI - list view - details view - modals: edit info, edit prompt, create - rename routes from `agents` to `assistatns` ### Server changes - Add `avatar` object to agent/assistnat saved object schema - changed schema from dynamic `strict` to `false` ### Recording https://github.com/user-attachments/assets/df689d87-2c0e-4e82-8dc1-46de4a9ab9d8 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
821f74ea5d
commit
c5ff7aa155
32 changed files with 1449 additions and 379 deletions
|
@ -3873,7 +3873,7 @@
|
|||
}
|
||||
},
|
||||
"workchat_agent": {
|
||||
"dynamic": "strict",
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"access_control": {
|
||||
"properties": {
|
||||
|
|
|
@ -18,11 +18,15 @@ export interface Agent {
|
|||
user: UserNameAndId;
|
||||
public: boolean;
|
||||
configuration: Record<string, any>;
|
||||
avatar: {
|
||||
color?: string;
|
||||
text?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AgentCreateRequest = Pick<
|
||||
Agent,
|
||||
'name' | 'description' | 'configuration' | 'public'
|
||||
'name' | 'description' | 'configuration' | 'public' | 'avatar'
|
||||
> & {
|
||||
id?: string;
|
||||
};
|
||||
|
|
|
@ -10,17 +10,16 @@
|
|||
*/
|
||||
export const appPaths = {
|
||||
home: '/',
|
||||
|
||||
chat: {
|
||||
new: ({ agentId }: { agentId: string }) => `/agents/${agentId}/chat`,
|
||||
new: ({ agentId }: { agentId: string }) => `/assistants/${agentId}/chat`,
|
||||
conversation: ({ agentId, conversationId }: { agentId: string; conversationId: string }) =>
|
||||
`/agents/${agentId}/chat/${conversationId}`,
|
||||
`/assistants/${agentId}/chat/${conversationId}`,
|
||||
},
|
||||
|
||||
agents: {
|
||||
list: '/agents',
|
||||
create: '/agents/create',
|
||||
edit: ({ agentId }: { agentId: string }) => `/agents/${agentId}/edit`,
|
||||
assistants: {
|
||||
list: '/assistants',
|
||||
create: '/assistants/create',
|
||||
edit: ({ agentId }: { agentId: string }) => `/assistants/${agentId}/edit`,
|
||||
workflow: ({ agentId }: { agentId: string }) => `/assistants/${agentId}/workflow`,
|
||||
},
|
||||
|
||||
integrations: {
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
* 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, { useMemo, useCallback } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFieldText,
|
||||
EuiTextArea,
|
||||
EuiFlexGroup,
|
||||
EuiPanel,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiDescribedFormGroup,
|
||||
} from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useNavigation } from '../../../hooks/use_navigation';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { useBreadcrumb } from '../../../hooks/use_breadcrumbs';
|
||||
import { useAgentEdition } from '../../../hooks/use_agent_edition';
|
||||
import { appPaths } from '../../../app_paths';
|
||||
import { agentLabels } from '../i18n';
|
||||
|
||||
interface AgentEditViewProps {
|
||||
agentId: string | undefined;
|
||||
}
|
||||
|
||||
export const AgentEditView: React.FC<AgentEditViewProps> = ({ agentId }) => {
|
||||
const { navigateToWorkchatUrl, createWorkchatUrl } = useNavigation();
|
||||
const {
|
||||
services: { notifications },
|
||||
} = useKibana();
|
||||
|
||||
const breadcrumb = useMemo(() => {
|
||||
return [
|
||||
{ text: agentLabels.breadcrumb.agentsPill, href: createWorkchatUrl(appPaths.agents.list) },
|
||||
agentId
|
||||
? { text: agentLabels.breadcrumb.editAgentPill }
|
||||
: { text: agentLabels.breadcrumb.createAgensPill },
|
||||
];
|
||||
}, [agentId, createWorkchatUrl]);
|
||||
|
||||
useBreadcrumb(breadcrumb);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
navigateToWorkchatUrl('/agents');
|
||||
}, [navigateToWorkchatUrl]);
|
||||
|
||||
const onSaveSuccess = useCallback(() => {
|
||||
notifications.toasts.addSuccess(
|
||||
agentId
|
||||
? agentLabels.notifications.agentUpdatedToastText
|
||||
: agentLabels.notifications.agentCreatedToastText
|
||||
);
|
||||
navigateToWorkchatUrl('/agents');
|
||||
}, [agentId, navigateToWorkchatUrl, notifications]);
|
||||
|
||||
const { editState, setFieldValue, submit, isSubmitting } = useAgentEdition({
|
||||
agentId,
|
||||
onSaveSuccess,
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
submit();
|
||||
},
|
||||
[submit, isSubmitting]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate panelled>
|
||||
<KibanaPageTemplate.Header
|
||||
pageTitle={
|
||||
agentId ? agentLabels.editView.editAgentTitle : agentLabels.editView.createAgentTitle
|
||||
}
|
||||
/>
|
||||
|
||||
<KibanaPageTemplate.Section grow={false} paddingSize="m">
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiForm component="form" fullWidth onSubmit={onSubmit}>
|
||||
<EuiDescribedFormGroup
|
||||
ratio="third"
|
||||
title={<h3>Base configuration</h3>}
|
||||
description="Configure your agent"
|
||||
>
|
||||
<EuiFormRow label="Name">
|
||||
<EuiFieldText
|
||||
data-test-subj="workchatAppAgentEditViewFieldText"
|
||||
name="name"
|
||||
value={editState.name}
|
||||
onChange={(e) => setFieldValue('name', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label="Description">
|
||||
<EuiFieldText
|
||||
data-test-subj="workchatAppAgentEditViewFieldText"
|
||||
name="description"
|
||||
value={editState.description}
|
||||
onChange={(e) => setFieldValue('description', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label="Visibility">
|
||||
<EuiSelect
|
||||
data-test-subj="workchatAppAgentEditViewSelect"
|
||||
name="public"
|
||||
value={editState.public ? 'public' : 'private'}
|
||||
options={[
|
||||
{ value: 'public', text: 'Public - everyone can use it' },
|
||||
{ value: 'private', text: 'Private - only you can use it' },
|
||||
]}
|
||||
onChange={(e) => setFieldValue('public', e.target.value === 'public')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiDescribedFormGroup
|
||||
ratio="third"
|
||||
title={<h3>Customization</h3>}
|
||||
description="Optional parameters to customize the agent"
|
||||
>
|
||||
<EuiFormRow label="System prompt">
|
||||
<EuiTextArea
|
||||
data-test-subj="workchatAppAgentEditViewTextArea"
|
||||
name="systemPrompt"
|
||||
value={editState.systemPrompt}
|
||||
onChange={(e) => setFieldValue('systemPrompt', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="workchatAppAgentEditViewCancelButton"
|
||||
type="button"
|
||||
iconType="framePrevious"
|
||||
color="warning"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{agentLabels.editView.cancelButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="workchatAppAgentEditViewSaveButton"
|
||||
type="submit"
|
||||
iconType="save"
|
||||
fill
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{agentLabels.editView.saveButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
};
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const agentLabels = {
|
||||
breadcrumb: {
|
||||
agentsPill: i18n.translate('workchatApp.agents.breadcrumb.agents', {
|
||||
defaultMessage: 'Agents',
|
||||
}),
|
||||
editAgentPill: i18n.translate('workchatApp.agents.breadcrumb.editAgent', {
|
||||
defaultMessage: 'Edit agent',
|
||||
}),
|
||||
createAgensPill: i18n.translate('workchatApp.agents.breadcrumb.createAgent', {
|
||||
defaultMessage: 'Create agent',
|
||||
}),
|
||||
},
|
||||
notifications: {
|
||||
agentCreatedToastText: i18n.translate(
|
||||
'workchatApp.agents.notifications.agentCreatedToastText',
|
||||
{
|
||||
defaultMessage: 'Agent created',
|
||||
}
|
||||
),
|
||||
agentUpdatedToastText: i18n.translate(
|
||||
'workchatApp.agents.notifications.agentCreatedToastText',
|
||||
{
|
||||
defaultMessage: 'Agent updated',
|
||||
}
|
||||
),
|
||||
},
|
||||
editView: {
|
||||
createAgentTitle: i18n.translate('workchatApp.agents.editView.createTitle', {
|
||||
defaultMessage: 'Create a new agent',
|
||||
}),
|
||||
editAgentTitle: i18n.translate('workchatApp.agents.editView.editTitle', {
|
||||
defaultMessage: 'Edit agent',
|
||||
}),
|
||||
cancelButtonLabel: i18n.translate('workchatApp.agents.editView.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
saveButtonLabel: i18n.translate('workchatApp.agents.editView.saveButtonLabel', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiBasicTable, EuiBasicTableColumn, EuiButton } from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { Agent } from '../../../../../common/agents';
|
||||
import { useNavigation } from '../../../hooks/use_navigation';
|
||||
import { appPaths } from '../../../app_paths';
|
||||
|
||||
interface AgentListViewProps {
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
export const AgentListView: React.FC<AgentListViewProps> = ({ agents }) => {
|
||||
const { navigateToWorkchatUrl } = useNavigation();
|
||||
const columns: Array<EuiBasicTableColumn<Agent>> = [
|
||||
{ field: 'name', name: 'Name' },
|
||||
{ field: 'description', name: 'Description' },
|
||||
{ field: 'user.name', name: 'Created by' },
|
||||
{ field: 'public', name: 'Public' },
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Edit',
|
||||
description: 'Edit this agent',
|
||||
isPrimary: true,
|
||||
icon: 'documentEdit',
|
||||
type: 'icon',
|
||||
onClick: ({ id }) => {
|
||||
navigateToWorkchatUrl(appPaths.agents.edit({ agentId: id }));
|
||||
},
|
||||
'data-test-subj': 'agentListTable-edit-btn',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate panelled>
|
||||
<KibanaPageTemplate.Header pageTitle="Agents" />
|
||||
|
||||
<KibanaPageTemplate.Section grow={false} paddingSize="m">
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
return navigateToWorkchatUrl('/agents/create');
|
||||
}}
|
||||
>
|
||||
Create new agent
|
||||
</EuiButton>
|
||||
</KibanaPageTemplate.Section>
|
||||
|
||||
<KibanaPageTemplate.Section>
|
||||
<EuiBasicTable columns={columns} items={agents} />
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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, { Fragment, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiFieldText,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormProvider, useForm, Controller } from 'react-hook-form';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { AgentEditState, useAgentEdition } from '../../hooks/use_agent_edition';
|
||||
import type { Agent } from '../../../../common/agents';
|
||||
import { appPaths } from '../../app_paths';
|
||||
import { useNavigation } from '../../hooks/use_navigation';
|
||||
import { assistantLabels } from './i18n';
|
||||
import { ASSISTANT_USE_CASES } from './constants';
|
||||
|
||||
export interface CreateNewAssistantModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CreateNewAssistantModal: React.FC<CreateNewAssistantModalProps> = ({ onClose }) => {
|
||||
const {
|
||||
services: { notifications },
|
||||
} = useKibana();
|
||||
|
||||
const { navigateToWorkchatUrl } = useNavigation();
|
||||
|
||||
const onSaveSuccess = useCallback(
|
||||
(agent: Agent) => {
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('workchatApp.assistants.createSuccessMessage', {
|
||||
defaultMessage: 'Assistant created successfully',
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
navigateToWorkchatUrl(appPaths.assistants.edit({ agentId: agent.id }));
|
||||
},
|
||||
[notifications, onClose, navigateToWorkchatUrl]
|
||||
);
|
||||
|
||||
const onSaveError = useCallback(
|
||||
(err: Error) => {
|
||||
notifications.toasts.addError(err, {
|
||||
title: 'Error',
|
||||
});
|
||||
},
|
||||
[notifications]
|
||||
);
|
||||
|
||||
const { state, isSubmitting, submit } = useAgentEdition({
|
||||
onSaveSuccess,
|
||||
onSaveError,
|
||||
});
|
||||
|
||||
const formMethods = useForm<AgentEditState>({
|
||||
values: state,
|
||||
});
|
||||
|
||||
const { handleSubmit, control, watch, setValue } = formMethods;
|
||||
|
||||
const useCase = watch('useCase');
|
||||
|
||||
useEffect(() => {
|
||||
if (useCase) {
|
||||
const selectedUseCase = ASSISTANT_USE_CASES.find((uc) => uc.value === useCase);
|
||||
if (selectedUseCase) {
|
||||
setValue('systemPrompt', selectedUseCase.prompt);
|
||||
}
|
||||
}
|
||||
}, [useCase, setValue]);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onClose} style={{ width: 640 }}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('workchatApp.assistants.create.title', {
|
||||
defaultMessage: 'Create new assistant',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<FormProvider {...formMethods}>
|
||||
<EuiForm component="form" onSubmit={handleSubmit((data) => submit(data))} fullWidth>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.create.nameLabel', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<Controller
|
||||
rules={{ required: true }}
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiFieldText data-test-subj="assistantNameInput" {...field} fullWidth />
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<Controller
|
||||
name="useCase"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editPromptModal.useCaseLabel', {
|
||||
defaultMessage: 'Use case',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="assistantUseCaseSelect"
|
||||
options={ASSISTANT_USE_CASES.map(({ label, value, description }) => ({
|
||||
inputDisplay: label,
|
||||
value,
|
||||
dropdownDisplay: (
|
||||
<Fragment>
|
||||
<strong>{label}</strong>
|
||||
{!!description && (
|
||||
<EuiText size="s" color="subdued">
|
||||
{description}
|
||||
</EuiText>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
}))}
|
||||
{...field}
|
||||
valueOfSelected={field.value}
|
||||
hasDividers
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
</EuiForm>
|
||||
</FormProvider>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{assistantLabels.editView.cancelButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="saveBasicInfoButton"
|
||||
fill
|
||||
onClick={handleSubmit((data) => submit(data))}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{assistantLabels.editView.saveButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ASSISTANT_USE_CASES = [
|
||||
{
|
||||
value: 'customerSupport',
|
||||
label: i18n.translate('workchatApp.assistants.editView.useCase.customerSupportLabel', {
|
||||
defaultMessage: 'Customer Support',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'workchatApp.assistants.editView.useCase.customerSupportDescription',
|
||||
{
|
||||
defaultMessage: 'Help customers with inquiries and support issues',
|
||||
}
|
||||
),
|
||||
prompt:
|
||||
'You are a helpful customer support assistant. Provide clear, accurate, and friendly responses to customer inquiries. Focus on resolving issues efficiently while maintaining a professional tone.',
|
||||
},
|
||||
{
|
||||
value: 'dataAnalysis',
|
||||
label: i18n.translate('workchatApp.assistants.editView.useCase.dataAnalysisLabel', {
|
||||
defaultMessage: 'Data Analysis',
|
||||
}),
|
||||
description: i18n.translate('workchatApp.assistants.editView.useCase.dataAnalysisDescription', {
|
||||
defaultMessage: 'Analyze and interpret data to provide insights',
|
||||
}),
|
||||
prompt:
|
||||
'You are a data analysis assistant. Help users understand their data by identifying patterns, trends, and anomalies. Provide clear explanations of your findings and suggest actionable insights.',
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: i18n.translate('workchatApp.assistants.editView.useCase.customLabel', {
|
||||
defaultMessage: 'Custom',
|
||||
}),
|
||||
description: i18n.translate('workchatApp.assistants.editView.useCase.customDescription', {
|
||||
defaultMessage: 'Create a custom assistant for your specific needs',
|
||||
}),
|
||||
prompt: '',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* 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, { useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiPanel,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiLoadingElastic,
|
||||
EuiAvatar,
|
||||
} from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useNavigation } from '../../../hooks/use_navigation';
|
||||
import { useBreadcrumb } from '../../../hooks/use_breadcrumbs';
|
||||
import { appPaths } from '../../../app_paths';
|
||||
import { assistantLabels } from '../i18n';
|
||||
import { useAgent } from '../../../hooks/use_agent';
|
||||
import { useConversationList } from '../../../hooks/use_conversation_list';
|
||||
import { sortAndGroupConversations } from '../../../utils/sort_and_group_conversations';
|
||||
import { sliceRecentConversations } from '../../../utils/slice_recent_conversations';
|
||||
import { EditAssistantBasicInfo } from './assistant_edit_basic_info_modal';
|
||||
import { EditPrompt } from './assistant_edit_prompt_modal';
|
||||
|
||||
interface AssistantDetailsProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export const AssistantDetails: React.FC<AssistantDetailsProps> = ({ agentId }) => {
|
||||
const { navigateToWorkchatUrl, createWorkchatUrl } = useNavigation();
|
||||
const { agent: assistant, isLoading, refetch } = useAgent({ agentId });
|
||||
const { conversations } = useConversationList({ agentId });
|
||||
|
||||
// State for modals
|
||||
const [isBasicInfoModalVisible, setIsBasicInfoModalVisible] = useState(false);
|
||||
const [isPromptModalVisible, setIsPromptModalVisible] = useState(false);
|
||||
|
||||
const conversationGroups = useMemo(() => {
|
||||
return sortAndGroupConversations(sliceRecentConversations(conversations, 10));
|
||||
}, [conversations]);
|
||||
|
||||
const breadcrumb = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
text: assistantLabels.breadcrumb.assistantsPill,
|
||||
href: createWorkchatUrl(appPaths.assistants.list),
|
||||
},
|
||||
{ text: assistantLabels.breadcrumb.assistantDetailsPill },
|
||||
];
|
||||
}, [createWorkchatUrl]);
|
||||
|
||||
useBreadcrumb(breadcrumb);
|
||||
|
||||
const handleOpenBasicInfoModal = useCallback(() => {
|
||||
setIsBasicInfoModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseBasicInfoModal = useCallback(() => {
|
||||
setIsBasicInfoModalVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleOpenPromptModal = useCallback(() => {
|
||||
setIsPromptModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleClosePromptModal = useCallback(() => {
|
||||
setIsPromptModalVisible(false);
|
||||
}, []);
|
||||
|
||||
const handleSaveSuccess = useCallback(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
if (isLoading || !assistant) {
|
||||
return (
|
||||
<KibanaPageTemplate.EmptyPrompt>
|
||||
<EuiLoadingElastic />
|
||||
</KibanaPageTemplate.EmptyPrompt>
|
||||
);
|
||||
}
|
||||
|
||||
const AssistantBasicInfo = () => (
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="flexStart" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" direction="row" gutterSize="xl">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar
|
||||
size="xl"
|
||||
name={assistant.name}
|
||||
initials={assistant.avatar?.text}
|
||||
color={assistant.avatar?.color}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<span>{assistant.name}</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
{assistant.description}
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconType="pencil"
|
||||
color="text"
|
||||
onClick={handleOpenBasicInfoModal}
|
||||
data-test-subj="editAssistantBasicInfoButton"
|
||||
>
|
||||
{assistantLabels.editView.editButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
const AssistantPrompt = () => (
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="flexStart" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="m" color="subdued">
|
||||
{assistant?.configuration?.systemPrompt}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconType="pencil"
|
||||
color="text"
|
||||
onClick={handleOpenPromptModal}
|
||||
data-test-subj="editAssistantPromptButton"
|
||||
>
|
||||
{assistantLabels.editView.editButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
const AssistantChatHistory = () => (
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiFlexGroup direction="column" gutterSize="l">
|
||||
{conversationGroups.map(({ conversations: groupConversations, dateLabel }) => (
|
||||
<EuiFlexItem key={dateLabel}>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} color="transparent" paddingSize="s">
|
||||
<EuiText size="s">
|
||||
<h4>{dateLabel}</h4>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{groupConversations.map((conversation) => (
|
||||
<EuiFlexItem key={conversation.id}>
|
||||
<EuiLink
|
||||
color="subdued"
|
||||
onClick={() => {
|
||||
navigateToWorkchatUrl(
|
||||
appPaths.chat.conversation({
|
||||
agentId: assistant.id,
|
||||
conversationId: conversation.id,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{conversation.title}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<KibanaPageTemplate.Section paddingSize="m">
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiFlexGroup gutterSize="xl" alignItems="flexStart" direction="row">
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFlexGroup gutterSize="l" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiIcon type="user" size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
{i18n.translate('workchatApp.assistants.basicInfoTitle', {
|
||||
defaultMessage: 'Basic',
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<AssistantBasicInfo />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiIcon type="gear" size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
{i18n.translate('workchatApp.assistants.promptTitle', {
|
||||
defaultMessage: 'Prompt',
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<AssistantPrompt />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiIcon type="list" size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
{i18n.translate('workchatApp.home.recentConversations.title', {
|
||||
defaultMessage: 'Recent conversations',
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<AssistantChatHistory />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</KibanaPageTemplate.Section>
|
||||
|
||||
{/* Modals */}
|
||||
{isBasicInfoModalVisible && (
|
||||
<EditAssistantBasicInfo
|
||||
agentId={agentId}
|
||||
onClose={handleCloseBasicInfoModal}
|
||||
onSaveSuccess={handleSaveSuccess}
|
||||
/>
|
||||
)}
|
||||
{isPromptModalVisible && (
|
||||
<EditPrompt
|
||||
agentId={agentId}
|
||||
onClose={handleClosePromptModal}
|
||||
onSaveSuccess={handleSaveSuccess}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* 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 { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSpacer,
|
||||
EuiTextArea,
|
||||
EuiColorPicker,
|
||||
EuiText,
|
||||
EuiFieldText,
|
||||
EuiFormHelpText,
|
||||
EuiAvatar,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiPaletteColorBlind } from '@elastic/eui';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { useAgentEdition } from '../../../hooks/use_agent_edition';
|
||||
import { assistantLabels } from '../i18n';
|
||||
|
||||
export interface EditAssistantBasicInfoProps {
|
||||
onClose: () => void;
|
||||
onSaveSuccess: () => void;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
const AVATAR_COLORS = euiPaletteColorBlind();
|
||||
|
||||
export const EditAssistantBasicInfo: React.FC<EditAssistantBasicInfoProps> = ({
|
||||
onClose,
|
||||
agentId,
|
||||
onSaveSuccess,
|
||||
}) => {
|
||||
const {
|
||||
services: { notifications },
|
||||
} = useKibana();
|
||||
|
||||
const { state, submit, isSubmitting } = useAgentEdition({
|
||||
agentId,
|
||||
onSaveSuccess: () => {
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('workchatApp.assistants.editBasicsModal.saveSuccessMessage', {
|
||||
defaultMessage: 'Assistant updated successfully',
|
||||
})
|
||||
);
|
||||
onSaveSuccess();
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const { control, handleSubmit, watch } = useForm({
|
||||
values: state,
|
||||
});
|
||||
|
||||
// Get form values for the avatar preview
|
||||
const name = watch('name');
|
||||
const avatarCustomText = watch('avatarCustomText');
|
||||
const avatarColor = watch('avatarColor');
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onClose} style={{ width: 800 }}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('workchatApp.assistants.editBasicsModal.title', {
|
||||
defaultMessage: 'Edit Assistant Basics',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiForm component="form" onSubmit={handleSubmit((data) => submit(data))} fullWidth>
|
||||
<EuiText>
|
||||
<h4>
|
||||
{i18n.translate('workchatApp.assistants.editBasicsModal.identificationSection', {
|
||||
defaultMessage: 'Identification',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editBasicsModal.nameLabel', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText data-test-subj="assistantNameInput" {...field} fullWidth />
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editBasicsModal.descriptionLabel', {
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="assistantDescriptionInput"
|
||||
{...field}
|
||||
fullWidth
|
||||
rows={6}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
<EuiFormHelpText>
|
||||
{i18n.translate('workchatApp.assistants.editBasicsModal.descriptionHelpText', {
|
||||
defaultMessage: 'Describe what this assistant is going to be used for.',
|
||||
})}
|
||||
</EuiFormHelpText>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiText>
|
||||
<h4>
|
||||
{i18n.translate('workchatApp.assistants.editBasicsModal.avatarSection', {
|
||||
defaultMessage: 'Avatar',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar initials={avatarCustomText} name={name} color={avatarColor} size="xl" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<Controller
|
||||
name="avatarColor"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editBasicsModal.colorLabel', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiColorPicker
|
||||
data-test-subj="assistantAvatarColorPicker"
|
||||
onChange={onChange}
|
||||
color={value}
|
||||
swatches={AVATAR_COLORS}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Controller
|
||||
name="avatarCustomText"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editBasicsModal.textLabel', {
|
||||
defaultMessage: 'Custom Text',
|
||||
})}
|
||||
helpText={i18n.translate(
|
||||
'workchatApp.assistants.editBasicsModal.emojiHelpText',
|
||||
{
|
||||
defaultMessage: 'Press CTRL + CMD + Space for emojis',
|
||||
}
|
||||
)}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="assistantAvatarTextField"
|
||||
{...field}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{assistantLabels.editView.cancelButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="saveBasicInfoButton"
|
||||
fill
|
||||
onClick={handleSubmit((data) => submit(data))}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{assistantLabels.editView.saveButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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, { Fragment, useEffect } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSpacer,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
EuiTextArea,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { useAgentEdition } from '../../../hooks/use_agent_edition';
|
||||
import { assistantLabels } from '../i18n';
|
||||
import { ASSISTANT_USE_CASES } from '../constants';
|
||||
|
||||
export interface EditPromptProps {
|
||||
onClose: () => void;
|
||||
onSaveSuccess: () => void;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export const EditPrompt: React.FC<EditPromptProps> = ({ onClose, onSaveSuccess, agentId }) => {
|
||||
const {
|
||||
services: { notifications },
|
||||
} = useKibana();
|
||||
|
||||
const { state, submit, isSubmitting } = useAgentEdition({
|
||||
agentId,
|
||||
onSaveSuccess: () => {
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('workchatApp.assistants.editPromptModal.saveSuccessMessage', {
|
||||
defaultMessage: 'Assistant updated successfully',
|
||||
})
|
||||
);
|
||||
onSaveSuccess();
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const { control, handleSubmit, watch, setValue } = useForm({
|
||||
values: state,
|
||||
});
|
||||
|
||||
const useCase = watch('useCase');
|
||||
|
||||
useEffect(() => {
|
||||
if (useCase && useCase !== 'custom') {
|
||||
const selectedUseCase = ASSISTANT_USE_CASES.find((uc) => uc.value === useCase);
|
||||
if (selectedUseCase && selectedUseCase.prompt) {
|
||||
setValue('systemPrompt', selectedUseCase.prompt);
|
||||
}
|
||||
}
|
||||
}, [useCase, setValue]);
|
||||
|
||||
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newPrompt = e.target.value;
|
||||
setValue('systemPrompt', newPrompt);
|
||||
|
||||
if (useCase !== 'custom') {
|
||||
setValue('useCase', 'custom');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onClose} style={{ width: 800 }}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('workchatApp.assistants.editPromptModal.title', {
|
||||
defaultMessage: 'Edit prompt',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiForm component="form" onSubmit={handleSubmit((data) => submit(data))} fullWidth>
|
||||
<Controller
|
||||
name="useCase"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editPromptModal.useCaseLabel', {
|
||||
defaultMessage: 'Use case',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="assistantUseCaseSelect"
|
||||
options={ASSISTANT_USE_CASES.map(({ label, value, description }) => ({
|
||||
inputDisplay: label,
|
||||
value,
|
||||
dropdownDisplay: (
|
||||
<Fragment>
|
||||
<strong>{label}</strong>
|
||||
{!!description && (
|
||||
<EuiText size="s" color="subdued">
|
||||
{description}
|
||||
</EuiText>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
}))}
|
||||
{...field}
|
||||
valueOfSelected={field.value}
|
||||
hasDividers
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<Controller
|
||||
name="systemPrompt"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('workchatApp.assistants.editPromptModal.promptLabel', {
|
||||
defaultMessage: 'Prompt',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="assistantPromptTextArea"
|
||||
{...field}
|
||||
onChange={handlePromptChange}
|
||||
fullWidth
|
||||
rows={8}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
/>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{assistantLabels.editView.cancelButtonLabel}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="savePromptButton"
|
||||
fill
|
||||
onClick={handleSubmit((data) => submit(data))}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{assistantLabels.editView.saveButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { useNavigation } from '../../../hooks/use_navigation';
|
||||
import { appPaths } from '../../../app_paths';
|
||||
import { AssistantDetails } from './assistant_details';
|
||||
import { AssistantWorkflow } from './assistant_workflow';
|
||||
import { useAgent } from '../../../hooks/use_agent';
|
||||
|
||||
interface AssistantViewProps {
|
||||
agentId: string;
|
||||
selectedTab: 'details' | 'workflow';
|
||||
}
|
||||
|
||||
export const AssistantView: React.FC<AssistantViewProps> = ({ agentId, selectedTab }) => {
|
||||
const { navigateToWorkchatUrl } = useNavigation();
|
||||
|
||||
const { agent } = useAgent({ agentId });
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: i18n.translate('workchatApp.assistant.tabs.overviewTitle', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
id: 'details',
|
||||
onClick: () => navigateToWorkchatUrl(appPaths.assistants.edit({ agentId })),
|
||||
},
|
||||
{
|
||||
label: i18n.translate('workchatApp.assistant.tabs.workflowTitle', {
|
||||
defaultMessage: 'Workflows',
|
||||
}),
|
||||
id: 'workflow',
|
||||
onClick: () => navigateToWorkchatUrl(appPaths.assistants.workflow({ agentId })),
|
||||
},
|
||||
];
|
||||
|
||||
const headerButtons = [
|
||||
<EuiButton
|
||||
iconType={'newChat'}
|
||||
color="primary"
|
||||
fill
|
||||
iconSide="left"
|
||||
onClick={() => navigateToWorkchatUrl(appPaths.chat.new({ agentId }))}
|
||||
>
|
||||
New conversation
|
||||
</EuiButton>,
|
||||
<EuiButtonEmpty iconType={'questionInCircle'} color="primary" iconSide="left" href="/">
|
||||
Learn more
|
||||
</EuiButtonEmpty>,
|
||||
];
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate data-test-subj="workChatEditAssistantPage">
|
||||
<KibanaPageTemplate.Header
|
||||
pageTitle={agent?.name}
|
||||
rightSideItems={headerButtons}
|
||||
tabs={tabs.map((tab) => {
|
||||
return {
|
||||
...tab,
|
||||
isSelected: tab.id === selectedTab,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
{selectedTab === 'details' && <AssistantDetails agentId={agentId} />}
|
||||
{selectedTab === 'workflow' && <AssistantWorkflow agentId={agentId} />}
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
};
|
|
@ -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, { useMemo } from 'react';
|
||||
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useNavigation } from '../../../hooks/use_navigation';
|
||||
import { assistantLabels } from '../i18n';
|
||||
import { appPaths } from '../../../app_paths';
|
||||
import { useBreadcrumb } from '../../../hooks/use_breadcrumbs';
|
||||
|
||||
interface AssistantWorkflowProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export const AssistantWorkflow: React.FC<AssistantWorkflowProps> = ({ agentId }) => {
|
||||
const { createWorkchatUrl } = useNavigation();
|
||||
|
||||
const breadcrumb = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
text: assistantLabels.breadcrumb.assistantsPill,
|
||||
href: createWorkchatUrl(appPaths.assistants.list),
|
||||
},
|
||||
{ text: assistantLabels.breadcrumb.assistantWorkflowPill },
|
||||
];
|
||||
}, [createWorkchatUrl]);
|
||||
|
||||
useBreadcrumb(breadcrumb);
|
||||
return <KibanaPageTemplate.Section paddingSize="m">todo...</KibanaPageTemplate.Section>;
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const assistantLabels = {
|
||||
breadcrumb: {
|
||||
assistantsPill: i18n.translate('workchatApp.assistants.breadcrumb.assistants', {
|
||||
defaultMessage: 'Assistants',
|
||||
}),
|
||||
assistantDetailsPill: i18n.translate('workchatApp.assistants.breadcrumb.assistantOverview', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
assistantWorkflowPill: i18n.translate('workchatApp.assistants.breadcrumb.assistantWorkflow', {
|
||||
defaultMessage: 'Workflows',
|
||||
}),
|
||||
},
|
||||
notifications: {
|
||||
assistantCreatedToastText: i18n.translate(
|
||||
'workchatApp.assistants.notifications.assistantCreatedToastText',
|
||||
{
|
||||
defaultMessage: 'Assistant created',
|
||||
}
|
||||
),
|
||||
assistantUpdatedToastText: i18n.translate(
|
||||
'workchatApp.assistants.notifications.assistantCreatedToastText',
|
||||
{
|
||||
defaultMessage: 'Assistant updated',
|
||||
}
|
||||
),
|
||||
},
|
||||
editView: {
|
||||
createassistantTitle: i18n.translate('workchatApp.assistants.editView.createTitle', {
|
||||
defaultMessage: 'Create a new assistant',
|
||||
}),
|
||||
editassistantTitle: i18n.translate('workchatApp.assistants.editView.editTitle', {
|
||||
defaultMessage: 'Edit assistant',
|
||||
}),
|
||||
cancelButtonLabel: i18n.translate('workchatApp.assistants.editView.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
}),
|
||||
saveButtonLabel: i18n.translate('workchatApp.assistants.editView.saveButtonLabel', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
editButtonLabel: i18n.translate('workchatApp.assistants.editView.editButtonLabel', {
|
||||
defaultMessage: 'Edit',
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
Comparators,
|
||||
Criteria,
|
||||
EuiAvatar,
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiSpacer,
|
||||
EuiTableSortingType,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Agent } from '../../../../../common/agents';
|
||||
import { useNavigation } from '../../../hooks/use_navigation';
|
||||
import { appPaths } from '../../../app_paths';
|
||||
import { CreateNewAssistantModal } from '../assistant_create_modal';
|
||||
|
||||
interface AssistantListViewProps {
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
export const AssistantListView: React.FC<AssistantListViewProps> = ({ agents }) => {
|
||||
const { navigateToWorkchatUrl } = useNavigation();
|
||||
|
||||
const [sortField, setSortField] = useState<keyof Agent>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<Agent>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
render: (name: Agent['name'], agent: Agent) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiAvatar
|
||||
size="m"
|
||||
name={agent.name}
|
||||
initials={agent.avatar?.text}
|
||||
color={agent.avatar?.color}
|
||||
/>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => navigateToWorkchatUrl(appPaths.assistants.edit({ agentId: agent.id }))}
|
||||
>
|
||||
{name}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{ field: 'user.name', name: 'Created by' },
|
||||
{ field: 'public', name: 'Visibility' },
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Edit',
|
||||
description: 'Edit this agent',
|
||||
isPrimary: true,
|
||||
icon: 'documentEdit',
|
||||
type: 'icon',
|
||||
onClick: ({ id }) => {
|
||||
navigateToWorkchatUrl(appPaths.assistants.edit({ agentId: id }));
|
||||
},
|
||||
'data-test-subj': 'agentListTable-edit-btn',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const findAgents = (
|
||||
agentsList: Agent[],
|
||||
currentPageIndex: number,
|
||||
currentPageSize: number,
|
||||
currentSortField: keyof Agent,
|
||||
currentSortDirection: 'asc' | 'desc'
|
||||
) => {
|
||||
let items;
|
||||
|
||||
if (currentSortField) {
|
||||
items = agentsList
|
||||
.slice(0)
|
||||
.sort(Comparators.property(currentSortField, Comparators.default(currentSortDirection)));
|
||||
} else {
|
||||
items = agentsList;
|
||||
}
|
||||
|
||||
let pageOfItems;
|
||||
|
||||
if (!currentPageIndex && !currentPageSize) {
|
||||
pageOfItems = items;
|
||||
} else {
|
||||
const startIndex = currentPageIndex * currentPageSize;
|
||||
pageOfItems = items.slice(
|
||||
startIndex,
|
||||
Math.min(startIndex + currentPageSize, agentsList.length)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
pageOfItems,
|
||||
totalItemCount: agentsList.length,
|
||||
};
|
||||
};
|
||||
|
||||
const headerButtons = [
|
||||
<EuiButton
|
||||
iconType={'plusInCircle'}
|
||||
color="primary"
|
||||
fill
|
||||
iconSide="left"
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
New
|
||||
</EuiButton>,
|
||||
<EuiButtonEmpty iconType={'questionInCircle'} color="primary" iconSide="left" href="/">
|
||||
Learn more
|
||||
</EuiButtonEmpty>,
|
||||
];
|
||||
|
||||
const onTableChange = ({ page, sort }: Criteria<Agent>) => {
|
||||
if (page) {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}
|
||||
if (sort) {
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
}
|
||||
};
|
||||
|
||||
const { pageOfItems, totalItemCount } = findAgents(
|
||||
agents,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection
|
||||
);
|
||||
|
||||
const sorting: EuiTableSortingType<Agent> = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
};
|
||||
|
||||
const resultsCount = (
|
||||
<FormattedMessage
|
||||
id="workchatApp.assistants.list.resultsCountLabel"
|
||||
defaultMessage="Showing {start}-{end} of {total}"
|
||||
values={{
|
||||
start: <strong>{pageSize * pageIndex + 1}</strong>,
|
||||
end: <strong>{pageSize * pageIndex + pageSize}</strong>,
|
||||
total: totalItemCount,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate data-test-subj="workChatAssistantsListPage">
|
||||
<KibanaPageTemplate.Header
|
||||
pageTitle={i18n.translate('workchatApp.assistants.pageTitle', {
|
||||
defaultMessage: 'Assistants',
|
||||
})}
|
||||
description={i18n.translate('workchatApp.assistants.pageDescription', {
|
||||
defaultMessage:
|
||||
'An assistant is an AI helper built around your data. It can search, summarize, and take action using your connected integrations. You can manage or create assistants from here.',
|
||||
})}
|
||||
rightSideItems={headerButtons}
|
||||
/>
|
||||
|
||||
<KibanaPageTemplate.Section>
|
||||
<EuiText size="xs">{resultsCount}</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={pageOfItems}
|
||||
sorting={sorting}
|
||||
onChange={onTableChange}
|
||||
pagination={pagination}
|
||||
/>
|
||||
{createModalOpen && <CreateNewAssistantModal onClose={() => setCreateModalOpen(false)} />}
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
};
|
|
@ -97,6 +97,8 @@ export const ChatNewConversationPrompt: React.FC<ChatNewConversationPromptProps>
|
|||
name={agent?.name ?? chatCommonLabels.assistant.defaultNameLabel}
|
||||
size="xl"
|
||||
type="user"
|
||||
initials={agent?.avatar?.text}
|
||||
color={agent?.avatar?.color}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -34,7 +34,13 @@ export const AssistantBlock: React.FC<AssistantBlockProps> = ({ agentId }) => {
|
|||
return (
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar name={agent?.name ?? 'Assistant'} size="l" type="user" />
|
||||
<EuiAvatar
|
||||
name={agent?.name ?? 'Assistant'}
|
||||
size="l"
|
||||
type="user"
|
||||
initials={agent?.avatar?.text}
|
||||
color={agent?.avatar?.color}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup
|
||||
|
|
|
@ -33,14 +33,19 @@ export const HomeAssistantsSection: React.FC<{}> = () => {
|
|||
<EuiPanel key={assistant.id} paddingSize="m" hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar size="l" name={assistant.name} />
|
||||
<EuiAvatar
|
||||
size="l"
|
||||
name={assistant.name}
|
||||
initials={assistant.avatar?.text}
|
||||
color={assistant.avatar?.color}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="gear"
|
||||
color="text"
|
||||
onClick={() => {
|
||||
navigateToWorkchatUrl(appPaths.agents.edit({ agentId: assistant.id }));
|
||||
navigateToWorkchatUrl(appPaths.assistants.edit({ agentId: assistant.id }));
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -28,8 +28,8 @@ import { appPaths } from '../../app_paths';
|
|||
|
||||
export const HomeConversationHistorySection: React.FC<{}> = () => {
|
||||
const { navigateToWorkchatUrl } = useNavigation();
|
||||
const { agents } = useAgentList();
|
||||
const { conversations } = useConversationList({});
|
||||
const { agents, isLoading: isAgentsLoading } = useAgentList();
|
||||
const { conversations, isLoading: isConversationHistoryLoading } = useConversationList({});
|
||||
|
||||
const agentMap = useMemo<Record<string, Agent>>(() => {
|
||||
return agents.reduce<Record<string, Agent>>((map, agent) => {
|
||||
|
@ -42,6 +42,10 @@ export const HomeConversationHistorySection: React.FC<{}> = () => {
|
|||
return sortAndGroupConversations(sliceRecentConversations(conversations, 10));
|
||||
}, [conversations]);
|
||||
|
||||
if (isAgentsLoading || isConversationHistoryLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recentConversations = conversationGroups.map(
|
||||
({ conversations: groupConversations, dateLabel }) => {
|
||||
return (
|
||||
|
@ -54,14 +58,21 @@ export const HomeConversationHistorySection: React.FC<{}> = () => {
|
|||
<EuiFlexGroup direction={'column'}>
|
||||
{groupConversations.map((conversation) => {
|
||||
const agent = agentMap[conversation.agentId];
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem key={conversation.id}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{agent && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar name={agent.name} size="s" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiAvatar
|
||||
name={agent.name}
|
||||
initials={agent.avatar.text}
|
||||
color={agent.avatar.color}
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem direction="column" grow={false}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
|
|
|
@ -12,7 +12,11 @@ import { queryKeys } from '../query_keys';
|
|||
export const useAgent = ({ agentId }: { agentId: string }) => {
|
||||
const { agentService } = useWorkChatServices();
|
||||
|
||||
const { data: agent, isLoading } = useQuery({
|
||||
const {
|
||||
data: agent,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.agents.details(agentId),
|
||||
queryFn: async () => {
|
||||
return agentService.get(agentId);
|
||||
|
@ -22,5 +26,6 @@ export const useAgent = ({ agentId }: { agentId: string }) => {
|
|||
return {
|
||||
agent,
|
||||
isLoading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,6 +13,9 @@ export interface AgentEditState {
|
|||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
avatarColor?: string;
|
||||
avatarCustomText?: string;
|
||||
useCase?: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
|
@ -21,6 +24,9 @@ const emptyState = (): AgentEditState => {
|
|||
name: '',
|
||||
description: '',
|
||||
systemPrompt: '',
|
||||
avatarColor: undefined,
|
||||
avatarCustomText: '',
|
||||
useCase: '',
|
||||
public: false,
|
||||
};
|
||||
};
|
||||
|
@ -28,63 +34,74 @@ const emptyState = (): AgentEditState => {
|
|||
export const useAgentEdition = ({
|
||||
agentId,
|
||||
onSaveSuccess,
|
||||
onSaveError,
|
||||
}: {
|
||||
agentId: string | undefined;
|
||||
agentId?: string;
|
||||
onSaveSuccess: (agent: Agent) => void;
|
||||
onSaveError?: (err: Error) => void;
|
||||
}) => {
|
||||
const { agentService } = useWorkChatServices();
|
||||
|
||||
const [editState, setEditState] = useState<AgentEditState>(emptyState());
|
||||
const [state, setState] = useState<AgentEditState>(emptyState());
|
||||
const [isSubmitting, setSubmitting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAgent = async () => {
|
||||
if (agentId) {
|
||||
const agent = await agentService.get(agentId);
|
||||
setEditState({
|
||||
setState({
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
systemPrompt: agent.configuration.systemPrompt ?? '',
|
||||
public: agent.public,
|
||||
useCase: agent.configuration.useCase ?? '',
|
||||
avatarColor: agent.avatar.color,
|
||||
avatarCustomText: agent.avatar.text ?? '',
|
||||
});
|
||||
}
|
||||
};
|
||||
fetchAgent();
|
||||
}, [agentId, agentService]);
|
||||
|
||||
const setFieldValue = <T extends keyof AgentEditState>(key: T, value: AgentEditState[T]) => {
|
||||
setEditState((previous) => ({ ...previous, [key]: value }));
|
||||
};
|
||||
const submit = useCallback(
|
||||
(updatedAgent: AgentEditState) => {
|
||||
setSubmitting(true);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
name: updatedAgent.name,
|
||||
description: updatedAgent.description,
|
||||
configuration: {
|
||||
systemPrompt: updatedAgent.systemPrompt,
|
||||
useCase: updatedAgent.useCase,
|
||||
},
|
||||
avatar: {
|
||||
color: updatedAgent.avatarColor,
|
||||
text: updatedAgent.avatarCustomText,
|
||||
},
|
||||
public: updatedAgent.public,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
name: editState.name,
|
||||
description: editState.description,
|
||||
configuration: {
|
||||
systemPrompt: editState.systemPrompt,
|
||||
},
|
||||
public: editState.public,
|
||||
};
|
||||
|
||||
(agentId ? agentService.update(agentId, payload) : agentService.create(payload)).then(
|
||||
(response) => {
|
||||
setSubmitting(false);
|
||||
if (response.success) {
|
||||
onSaveSuccess(response.agent);
|
||||
(agentId ? agentService.update(agentId, payload) : agentService.create(payload)).then(
|
||||
(response) => {
|
||||
setSubmitting(false);
|
||||
if (response.success) {
|
||||
onSaveSuccess(response.agent);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setSubmitting(false);
|
||||
if (onSaveError) {
|
||||
onSaveError(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setSubmitting(false);
|
||||
}
|
||||
);
|
||||
}, [agentId, editState, agentService, onSaveSuccess]);
|
||||
);
|
||||
},
|
||||
[agentId, agentService, onSaveSuccess, onSaveError]
|
||||
);
|
||||
|
||||
return {
|
||||
editState,
|
||||
state,
|
||||
isSubmitting,
|
||||
setFieldValue,
|
||||
submit,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AgentEditView } from '../components/agents/edition/agent_edit_view';
|
||||
|
||||
const newAgentId = 'create';
|
||||
|
||||
export const WorkChatAgentEditOrCreatePage: React.FC<{}> = () => {
|
||||
const { agentId: agentIdFromParams } = useParams<{
|
||||
agentId: string;
|
||||
}>();
|
||||
|
||||
const agentId = useMemo(() => {
|
||||
return agentIdFromParams === newAgentId ? undefined : agentIdFromParams;
|
||||
}, [agentIdFromParams]);
|
||||
|
||||
return <AgentEditView agentId={agentId} />;
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import { AssistantView } from '../components/assistant/details/assistant_view';
|
||||
|
||||
export const WorkChatAssistantOverviewPage: React.FC<{}> = () => {
|
||||
const { agentId } = useParams<{
|
||||
agentId: string;
|
||||
}>();
|
||||
|
||||
return <AssistantView agentId={agentId} selectedTab="details" />;
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import { AssistantView } from '../components/assistant/details/assistant_view';
|
||||
|
||||
export const WorkChatAssistantWorkflowPage: React.FC<{}> = () => {
|
||||
const { agentId } = useParams<{
|
||||
agentId: string;
|
||||
}>();
|
||||
|
||||
return <AssistantView agentId={agentId} selectedTab="workflow" />;
|
||||
};
|
|
@ -8,10 +8,10 @@
|
|||
import React from 'react';
|
||||
import { useBreadcrumb } from '../hooks/use_breadcrumbs';
|
||||
import { useAgentList } from '../hooks/use_agent_list';
|
||||
import { AgentListView } from '../components/agents/listing/agent_list_view';
|
||||
import { AssistantListView } from '../components/assistant/list/assistant_list_view';
|
||||
|
||||
export const WorkChatAgentsPage: React.FC<{}> = () => {
|
||||
useBreadcrumb([{ text: 'Agents' }]);
|
||||
export const WorkChatAssistantsPage: React.FC<{}> = () => {
|
||||
useBreadcrumb([{ text: 'Assistants' }]);
|
||||
const { agents } = useAgentList();
|
||||
return <AgentListView agents={agents} />;
|
||||
return <AssistantListView agents={agents} />;
|
||||
};
|
|
@ -26,7 +26,7 @@ export const registerApp = ({
|
|||
title: 'WorkChat',
|
||||
updater$: undefined,
|
||||
deepLinks: [
|
||||
{ id: 'agents', path: '/agents', title: 'Agents' },
|
||||
{ id: 'agents', path: '/assistants', title: 'Assistants' },
|
||||
{ id: 'integrations', path: '/integrations', title: 'Integrations' },
|
||||
],
|
||||
visibleIn: ['sideNav', 'globalSearch'],
|
||||
|
|
|
@ -9,28 +9,32 @@ import React from 'react';
|
|||
import { Route, Routes } from '@kbn/shared-ux-router';
|
||||
import { WorkChatHomePage } from './pages/home';
|
||||
import { WorkchatChatPage } from './pages/chat';
|
||||
import { WorkChatAgentsPage } from './pages/agents';
|
||||
import { WorkChatAgentEditOrCreatePage } from './pages/agent_edit_or_create';
|
||||
import { WorkChatAssistantsPage } from './pages/assistants';
|
||||
import { WorkChatAssistantOverviewPage } from './pages/assistant_details';
|
||||
import { WorkChatIntegrationsPage } from './pages/integrations';
|
||||
import { WorkChatIntegrationEditOrCreatePage } from './pages/integration_edit_or_create';
|
||||
import { WorkChatAssistantWorkflowPage } from './pages/assistant_workflow';
|
||||
export const WorkchatAppRoutes: React.FC<{}> = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/agents/:agentId/chat/:conversationId">
|
||||
<Route path="/assistants/:agentId/chat/:conversationId">
|
||||
<WorkchatChatPage />
|
||||
</Route>
|
||||
<Route path="/agents/:agentId/chat">
|
||||
<Route path="/assistants/:agentId/chat">
|
||||
<WorkchatChatPage />
|
||||
</Route>
|
||||
|
||||
<Route path="/agents/create" strict>
|
||||
<WorkChatAgentEditOrCreatePage />
|
||||
<Route path="/assistants/create" strict>
|
||||
<WorkChatAssistantOverviewPage />
|
||||
</Route>
|
||||
<Route path="/agents/:agentId/edit" strict>
|
||||
<WorkChatAgentEditOrCreatePage />
|
||||
<Route path="/assistants/:agentId/edit" strict>
|
||||
<WorkChatAssistantOverviewPage />
|
||||
</Route>
|
||||
<Route path="/agents" strict>
|
||||
<WorkChatAgentsPage />
|
||||
<Route path="/assistants/:agentId/workflow" strict>
|
||||
<WorkChatAssistantWorkflowPage />
|
||||
</Route>
|
||||
<Route path="/assistants" strict>
|
||||
<WorkChatAssistantsPage />
|
||||
</Route>
|
||||
|
||||
<Route path="/integrations/create">
|
||||
|
|
|
@ -63,8 +63,13 @@ export const registerAgentRoutes = ({ getServices, router, logger }: RouteDepend
|
|||
description: schema.string({ defaultValue: '' }),
|
||||
configuration: schema.object({
|
||||
systemPrompt: schema.maybe(schema.string()),
|
||||
useCase: schema.maybe(schema.string()),
|
||||
}),
|
||||
public: schema.boolean({ defaultValue: false }),
|
||||
avatar: schema.object({
|
||||
color: schema.maybe(schema.string()),
|
||||
text: schema.maybe(schema.string()),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
@ -105,6 +110,11 @@ export const registerAgentRoutes = ({ getServices, router, logger }: RouteDepend
|
|||
description: schema.string({ defaultValue: '' }),
|
||||
configuration: schema.object({
|
||||
systemPrompt: schema.maybe(schema.string()),
|
||||
useCase: schema.maybe(schema.string()),
|
||||
}),
|
||||
avatar: schema.object({
|
||||
color: schema.maybe(schema.string()),
|
||||
text: schema.maybe(schema.string()),
|
||||
}),
|
||||
public: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
|
|
|
@ -15,7 +15,7 @@ export const agentSoType: SavedObjectsType<AgentAttributes> = {
|
|||
hidden: true,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
dynamic: 'strict',
|
||||
dynamic: false,
|
||||
properties: {
|
||||
agent_id: { type: 'keyword' },
|
||||
agent_name: { type: 'text' },
|
||||
|
@ -44,4 +44,8 @@ export interface AgentAttributes {
|
|||
access_control: {
|
||||
public: boolean;
|
||||
};
|
||||
avatar: {
|
||||
color?: string;
|
||||
text?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { agentTypeName, type AgentAttributes } from '../../saved_objects/agents'
|
|||
import { WorkchatError } from '../../errors';
|
||||
import { createBuilder } from '../../utils/so_filters';
|
||||
import { savedObjectToModel, createRequestToRaw, updateToAttributes } from './convert_model';
|
||||
import { getRandomColorFromPalette } from '../../utils/color';
|
||||
|
||||
interface AgentClientOptions {
|
||||
logger: Logger;
|
||||
|
@ -21,7 +22,7 @@ interface AgentClientOptions {
|
|||
}
|
||||
|
||||
export type AgentUpdatableFields = Partial<
|
||||
Pick<Agent, 'name' | 'description' | 'configuration' | 'public'>
|
||||
Pick<Agent, 'name' | 'description' | 'configuration' | 'public' | 'avatar'>
|
||||
>;
|
||||
|
||||
/**
|
||||
|
@ -83,12 +84,15 @@ export class AgentClientImpl implements AgentClient {
|
|||
async create(createRequest: AgentCreateRequest): Promise<Agent> {
|
||||
const now = new Date();
|
||||
const id = createRequest.id ?? uuidv4();
|
||||
const color = createRequest.avatar?.color ?? getRandomColorFromPalette();
|
||||
const attributes = createRequestToRaw({
|
||||
createRequest,
|
||||
id,
|
||||
user: this.user,
|
||||
creationDate: now,
|
||||
color,
|
||||
});
|
||||
|
||||
const created = await this.client.create<AgentAttributes>(agentTypeName, attributes, { id });
|
||||
return savedObjectToModel(created);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,10 @@ export const savedObjectToModel = ({ attributes }: SavedObject<AgentAttributes>)
|
|||
},
|
||||
configuration: attributes.configuration,
|
||||
public: attributes.access_control.public,
|
||||
avatar: {
|
||||
color: attributes.avatar?.color,
|
||||
text: attributes.avatar?.text,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -35,6 +39,7 @@ export const updateToAttributes = ({
|
|||
agent_name: updatedFields.name,
|
||||
description: updatedFields.description,
|
||||
configuration: updatedFields.configuration,
|
||||
avatar: updatedFields.avatar,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -43,11 +48,13 @@ export const createRequestToRaw = ({
|
|||
id,
|
||||
user,
|
||||
creationDate,
|
||||
color,
|
||||
}: {
|
||||
createRequest: AgentCreateRequest;
|
||||
id: string;
|
||||
user: UserNameAndId;
|
||||
creationDate: Date;
|
||||
color: string;
|
||||
}): AgentAttributes => {
|
||||
return {
|
||||
agent_id: id,
|
||||
|
@ -60,5 +67,9 @@ export const createRequestToRaw = ({
|
|||
access_control: {
|
||||
public: createRequest.public,
|
||||
},
|
||||
avatar: {
|
||||
color: color || createRequest.avatar.color,
|
||||
text: createRequest.avatar.text,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { euiPaletteColorBlind } from '@elastic/eui';
|
||||
|
||||
export const getRandomColorFromPalette = (): string => {
|
||||
const palette = euiPaletteColorBlind();
|
||||
return palette[Math.floor(Math.random() * palette.length)];
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue