Skip to content

Commit

Permalink
added terms and conditions to chat window (#1)
Browse files Browse the repository at this point in the history
Every user needs to accept the terms and conditions to use the chat
assistant

TODO: waiting for terms and conditions details page

---------

Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl committed Nov 20, 2023
1 parent 89e9e8a commit 3d7a470
Show file tree
Hide file tree
Showing 17 changed files with 279 additions and 39 deletions.
6 changes: 6 additions & 0 deletions common/constants/saved_objects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export const CHAT_CONFIG_SAVED_OBJECT_TYPE = 'chat-config';
2 changes: 1 addition & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "assistantDashboards",
"version": "2.9.0.0",
"version": "2.11.0.0",
"opensearchDashboardsVersion": "opensearchDashboards",
"server": true,
"ui": true,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "assistant-dashboards",
"version": "2.9.0.0",
"version": "2.11.0.0",
"main": "index.ts",
"license": "Apache-2.0",
"scripts": {
Expand Down Expand Up @@ -32,7 +32,7 @@
"@types/jsdom": "^21.1.2",
"@types/react-test-renderer": "^16.9.1",
"eslint": "^6.8.0",
"husky": "6.0.0",
"husky": "^8.0.0",
"jest-dom": "^4.0.0",
"lint-staged": "^13.1.0",
"ts-jest": "^29.1.0"
Expand Down
5 changes: 4 additions & 1 deletion public/chat_header_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { SetContext } from './contexts/set_context';
import { ChatStateProvider } from './hooks/use_chat_state';
import './index.scss';
import { TabId } from './tabs/chat_tab_bar';
import { ActionExecutor, AssistantActions, ContentRenderer } from './types';
import { ActionExecutor, AssistantActions, ContentRenderer, UserAccount } from './types';

interface HeaderChatButtonProps {
application: ApplicationStart;
chatEnabled: boolean;
contentRenderers: Record<string, ContentRenderer>;
actionExecutors: Record<string, ActionExecutor>;
assistantActions: AssistantActions;
currentAccount: UserAccount;
}

let flyoutLoaded = false;
Expand Down Expand Up @@ -61,6 +62,7 @@ export const HeaderChatButton: React.FC<HeaderChatButtonProps> = (props) => {
chatEnabled: props.chatEnabled,
contentRenderers: props.contentRenderers,
actionExecutors: props.actionExecutors,
currentAccount: props.currentAccount,
}),
[
appId,
Expand All @@ -70,6 +72,7 @@ export const HeaderChatButton: React.FC<HeaderChatButtonProps> = (props) => {
props.chatEnabled,
props.contentRenderers,
props.actionExecutors,
props.currentAccount,
]
);

Expand Down
67 changes: 67 additions & 0 deletions public/components/terms_and_conditions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { useObservable } from 'react-use';
import { EuiButton, EuiEmptyPrompt, EuiLink, EuiText } from '@elastic/eui';
import { SavedObjectManager } from '../services/saved_object_manager';
import { useCore } from '../contexts/core_context';
import { ChatConfig } from '../types';
import { CHAT_CONFIG_SAVED_OBJECT_TYPE } from '../../common/constants/saved_objects';

interface Props {
username: string;
}

export const TermsAndConditions = (props: Props) => {
const core = useCore();

const chatConfigService = SavedObjectManager.getInstance<ChatConfig>(
core.services.savedObjects.client,
CHAT_CONFIG_SAVED_OBJECT_TYPE
);
const config = useObservable(chatConfigService.get$(props.username));
const loading = useObservable(chatConfigService.getLoadingStatus$(props.username));
const termsAccepted = Boolean(config?.terms_accepted);

return (
<EuiEmptyPrompt
style={{ padding: 0 }}
iconType="cheer"
iconColor="primary"
titleSize="s"
body={
<EuiText>
<p>Welcome {props.username} to the OpenSearch Assistant</p>
<p>I can help you analyze data, create visualizations, and get other insights.</p>
<p>How can I help?</p>
<EuiText size="xs" color="subdued">
The OpenSearch Assistant may produce inaccurate information. Verify all information
before using it in any environment or workload.
</EuiText>
</EuiText>
}
actions={[
!termsAccepted && (
<EuiButton
isLoading={loading}
color="primary"
fill
onClick={() =>
chatConfigService.createOrUpdate(props.username, { terms_accepted: true })
}
>
Accept terms & go
</EuiButton>
),
<EuiText size="xs">
<EuiLink target="_blank" href="/">
Terms & Conditions
</EuiLink>
</EuiText>,
]}
/>
);
};
3 changes: 2 additions & 1 deletion public/contexts/chat_context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import React, { useContext } from 'react';
import { TabId } from '../tabs/chat_tab_bar';
import { ActionExecutor, ContentRenderer } from '../types';
import { ActionExecutor, ContentRenderer, UserAccount } from '../types';

export interface IChatContext {
appId?: string;
Expand All @@ -19,6 +19,7 @@ export interface IChatContext {
chatEnabled: boolean;
contentRenderers: Record<string, ContentRenderer>;
actionExecutors: Record<string, ActionExecutor>;
currentAccount: UserAccount;
}
export const ChatContext = React.createContext<IChatContext | null>(null);

Expand Down
13 changes: 4 additions & 9 deletions public/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@
.euiFlyoutFooter {
background: transparent;
}
.euiPage {
background: transparent;
}
}

.llm-chat-flyout-header {
Expand All @@ -78,7 +75,7 @@
&.llm-chat-bubble-panel {
word-break: break-word;
border-radius: 8px;
max-width: 80%;
max-width: 95%;
}
&.llm-chat-greeting-card-panel {
width: 357px;
Expand All @@ -102,13 +99,10 @@
}

.llm-chat-bubble-panel.llm-chat-bubble-panel-input {
background: #57c3ff;
border-color: white;
background: #159d8d;
margin-left: auto;
}
.llm-chat-bubble-panel.llm-chat-bubble-panel-output {
background: #e6f0f8;
border-color: white;
margin-right: auto;
}

Expand All @@ -126,7 +120,8 @@
}

.llm-chat-fullscreen {
.euiFlyoutBody__overflowContent, .euiFlyoutFooter {
.euiFlyoutBody__overflowContent,
.euiFlyoutFooter {
width: 70%;
margin: auto;
}
Expand Down
22 changes: 16 additions & 6 deletions public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,19 @@ export class AssistantPlugin
const contentRenderers: Record<string, ContentRenderer> = {};
const actionExecutors: Record<string, ActionExecutor> = {};
const assistantActions: AssistantActions = {} as AssistantActions;
const getAccount = async () => {
return await core.http.get<{ data: { roles: string[]; user_name: string } }>(
'/api/v1/configuration/account'
);
};
const assistantEnabled = (() => {
let enabled: boolean;
return async () => {
if (enabled === undefined) {
enabled = await core.http
.get<{ data: { roles: string[] } }>('/api/v1/configuration/account')
.then((res) =>
res.data.roles.some((role) => ['all_access', 'assistant_user'].includes(role))
);
const account = await getAccount();
enabled = account.data.roles.some((role) =>
['all_access', 'assistant_user'].includes(role)
);
}
return enabled;
};
Expand All @@ -53,16 +57,22 @@ export class AssistantPlugin
setupDeps,
startDeps,
});
const account = await getAccount();
const username = account.data.user_name;

coreStart.chrome.navControls.registerRight({
order: 10000,
mount: toMountPoint(
<CoreContext.Provider>
<HeaderChatButton
application={coreStart.application}
chatEnabled={await assistantEnabled()}
chatEnabled={account.data.roles.some((role) =>
['all_access', 'assistant_user'].includes(role)
)}
contentRenderers={contentRenderers}
actionExecutors={actionExecutors}
assistantActions={assistantActions}
currentAccount={{ username }}
/>
</CoreContext.Provider>
),
Expand Down
25 changes: 25 additions & 0 deletions public/services/saved_object_manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsClientContract } from '../../../../src/core/public';
import { SavedObjectService } from './saved_object_service';

export class SavedObjectManager {
private static instances: Map<string, SavedObjectService<{}>> = new Map();
private constructor() {}

public static getInstance<T extends {}>(
savedObjectsClient: SavedObjectsClientContract,
savedObjectType: string
) {
if (!SavedObjectManager.instances.has(savedObjectType)) {
SavedObjectManager.instances.set(
savedObjectType,
new SavedObjectService<T>(savedObjectsClient, savedObjectType)
);
}
return SavedObjectManager.instances.get(savedObjectType) as SavedObjectService<T>;
}
}
93 changes: 93 additions & 0 deletions public/services/saved_object_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { BehaviorSubject } from 'rxjs';
import { SavedObjectsClientContract } from '../../../../src/core/public';

export class SavedObjectService<T extends {}> {
private objects: Record<string, BehaviorSubject<Partial<T> | null>> = {};
private loadingStatus: Record<string, BehaviorSubject<boolean>> = {};

constructor(
private readonly client: SavedObjectsClientContract,
private readonly savedObjectType: string
) {}

private setLoading(id: string, loading: boolean) {
if (!this.loadingStatus[id]) {
this.loadingStatus[id] = new BehaviorSubject(loading);
} else {
this.loadingStatus[id].next(loading);
}
}

private async load(id: string) {
// set loading to true
this.setLoading(id, true);

const savedObject = await this.client.get<Partial<T>>(this.savedObjectType, id);

// set loading to false
this.setLoading(id, false);

if (!savedObject.error) {
this.objects[id].next(savedObject.attributes);
}
return savedObject;
}

private async create(id: string, attributes: Partial<T>) {
this.setLoading(id, true);
const newObject = await this.client.create<Partial<T>>(this.savedObjectType, attributes, {
id,
});
this.objects[id].next({ ...newObject.attributes });
this.setLoading(id, false);
return newObject.attributes;
}

private async update(id: string, attributes: Partial<T>) {
this.setLoading(id, true);
const newObject = await this.client.update<Partial<T>>(this.savedObjectType, id, attributes);
this.objects[id].next({ ...newObject.attributes });
this.setLoading(id, false);
return newObject.attributes;
}

private async initialize(id: string) {
if (!this.objects[id]) {
this.objects[id] = new BehaviorSubject<Partial<T> | null>(null);
await this.load(id);
}
}

public async get(id: string) {
await this.initialize(id);
return this.objects[id].getValue();
}

public get$(id: string) {
this.initialize(id);
return this.objects[id];
}

public getLoadingStatus$(id: string) {
return this.loadingStatus[id];
}

public async createOrUpdate(id: string, attributes: Partial<T>) {
const currentObject = await this.load(id);

if (currentObject.error) {
// Object not found, create a new object
if (currentObject.error.statusCode === 404) {
return await this.create(id, attributes);
}
} else {
// object found, update existing object
return await this.update(id, attributes);
}
}
}
Loading

0 comments on commit 3d7a470

Please sign in to comment.