@finos/legend-application
Version:
Legend application core
332 lines (297 loc) • 9.86 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { action, makeObservable, observable, computed } from 'mobx';
import type { GenericLegendApplicationStore } from './ApplicationStore.js';
import {
type MarkdownText,
guaranteeNonEmptyString,
uuid,
isNonNullable,
ActionState,
FuzzySearchEngine,
FuzzySearchAdvancedConfigState,
type DocumentationEntry,
} from '@finos/legend-shared';
export enum VIRTUAL_ASSISTANT_TAB {
SEARCH = 'SEARCH',
CONTEXTUAL_SUPPORT = 'CONTEXTUAL_SUPPORT',
}
export const DOCUMENTATION_SEARCH_RESULTS_LIMIT = 100;
export class VirtualAssistantDocumentationEntry {
uuid = uuid();
documentationKey: string;
title: string;
content?: string | MarkdownText | undefined;
url?: string | undefined;
isOpen = false;
constructor(docEntry: DocumentationEntry) {
makeObservable(this, {
isOpen: observable,
setIsOpen: action,
});
this.documentationKey = docEntry.key;
this.title = guaranteeNonEmptyString(docEntry.title);
this.content = docEntry.markdownText ?? docEntry.text;
this.url = docEntry.url;
}
setIsOpen(val: boolean): void {
this.isOpen = val;
}
}
export class VirtualAssistantContextualDocumentationEntry {
uuid = uuid();
context: string;
title?: string | undefined;
content?: string | MarkdownText | undefined;
url?: string | undefined;
related: VirtualAssistantDocumentationEntry[];
constructor(
context: string,
docEntry: DocumentationEntry,
related: VirtualAssistantDocumentationEntry[],
) {
this.context = context;
this.title = docEntry.title;
this.content = docEntry.markdownText ?? docEntry.text;
this.url = docEntry.url;
this.related = related;
}
}
/**
* NOTE: since we're displaying the documentation entry in virtual assistant
* we want only user-friendly docs, we will discard anything that doesn't
* come with a title, or does not have any content/url
*/
export const isValidVirtualAssistantDocumentationEntry = (
entry: DocumentationEntry,
): boolean =>
Boolean(entry.title && (entry.url ?? entry.text ?? entry.markdownText));
/**
* Check if the documentation entry should be displayed in virtual assistant,
* i.e. it has some text content, rather just a link
*/
export const shouldDisplayVirtualAssistantDocumentationEntry = (
entry: DocumentationEntry,
): boolean =>
isValidVirtualAssistantDocumentationEntry(entry) &&
Boolean(entry.text ?? entry.markdownText);
export class AssistantService {
readonly applicationStore: GenericLegendApplicationStore;
/**
* This key is used to allow programmatic re-rendering of the assistant panel
*/
panelRenderingKey = uuid();
isDisabled = false;
isHidden = true; // hide by default unless specified by the application to show
isOpen = false;
isPanelMaximized = false;
selectedTab: string = VIRTUAL_ASSISTANT_TAB.SEARCH;
currentDocumentationEntry: VirtualAssistantDocumentationEntry | undefined;
// search text
private readonly searchEngine: FuzzySearchEngine<DocumentationEntry>;
searchConfigurationState: FuzzySearchAdvancedConfigState;
readonly searchState = ActionState.create();
searchText = '';
searchResults: VirtualAssistantDocumentationEntry[] = [];
showSearchConfigurationMenu = false;
isOverSearchLimit = false;
constructor(applicationStore: GenericLegendApplicationStore) {
makeObservable(this, {
isDisabled: observable,
isHidden: observable,
isOpen: observable,
isPanelMaximized: observable,
panelRenderingKey: observable,
isOverSearchLimit: observable,
selectedTab: observable,
searchText: observable,
searchResults: observable,
currentDocumentationEntry: observable,
showSearchConfigurationMenu: observable,
currentContextualDocumentationEntry: computed,
setIsDisabled: action,
setIsHidden: action,
setIsOpen: action,
setIsPanelMaximized: action,
setSelectedTab: action,
setSearchText: action,
resetSearch: action,
search: action,
openDocumentationEntry: action,
refreshPanelRendering: action,
setShowSearchConfigurationMenu: action,
});
this.applicationStore = applicationStore;
this.searchEngine = new FuzzySearchEngine(
this.applicationStore.documentationService
.getAllDocEntries()
.filter(isValidVirtualAssistantDocumentationEntry),
{
includeScore: true,
shouldSort: true,
// Ignore location when computing the search score
// See https://fusejs.io/concepts/scoring-theory.html
ignoreLocation: true,
// This specifies the point the search gives up
// `0.0` means exact match where `1.0` would match anything
// We set a relatively low threshold to filter out irrelevant results
threshold: 0.2,
keys: [
{
// NOTE: for now, we would weight title the most
name: 'title',
weight: 4,
},
{
name: 'text',
weight: 1,
},
{
name: 'markdownText.value',
weight: 1,
},
],
// extended search allows for exact word match through single quote
// See https://fusejs.io/examples.html#extended-search
useExtendedSearch: true,
},
);
this.searchConfigurationState = new FuzzySearchAdvancedConfigState(() => {
this.search();
});
}
get currentContextualDocumentationEntry():
| VirtualAssistantContextualDocumentationEntry
| undefined {
if (!this.applicationStore.navigationContextService.currentContext) {
return undefined;
}
const currentContext =
this.applicationStore.navigationContextService.currentContext.key;
const currentContextualDocumentationEntry =
this.applicationStore.documentationService.getContextualDocEntry(
currentContext,
);
return currentContextualDocumentationEntry
? new VirtualAssistantContextualDocumentationEntry(
currentContext,
currentContextualDocumentationEntry,
(currentContextualDocumentationEntry.related ?? [])
.map((entry) =>
this.applicationStore.documentationService.getDocEntry(entry),
)
.filter(isNonNullable)
.filter(isValidVirtualAssistantDocumentationEntry)
.map((entry) => new VirtualAssistantDocumentationEntry(entry)),
)
: undefined;
}
openDocumentationEntry(key: string): void {
const entry = this.applicationStore.documentationService.getDocEntry(key);
if (entry) {
this.setIsOpen(true);
this.setIsHidden(false);
this.currentDocumentationEntry = new VirtualAssistantDocumentationEntry(
entry,
);
this.currentDocumentationEntry.setIsOpen(true);
this.resetSearch();
}
}
openDocumentationEntryLink(key: string): void {
const entry = this.applicationStore.documentationService.getDocEntry(key);
if (entry) {
if (shouldDisplayVirtualAssistantDocumentationEntry(entry)) {
this.openDocumentationEntry(entry.key);
} else if (entry.url) {
this.applicationStore.navigationService.navigator.visitAddress(
entry.url,
);
}
}
}
setIsDisabled(val: boolean): void {
this.isDisabled = val;
}
setIsHidden(val: boolean): void {
this.isHidden = val;
}
setIsPanelMaximized(val: boolean): void {
this.isPanelMaximized = val;
}
hideAssistant(): void {
this.setIsHidden(true);
this.setIsOpen(false);
}
toggleAssistant(): void {
const newVal = !this.isHidden;
if (newVal) {
this.hideAssistant();
} else {
this.setIsHidden(false);
}
}
setIsOpen(val: boolean): void {
this.isOpen = val;
}
setSelectedTab(val: string): void {
this.selectedTab = val;
}
refreshPanelRendering(): void {
this.panelRenderingKey = uuid();
}
setSearchText(val: string): void {
this.searchText = val;
}
resetSearch(): void {
this.searchText = '';
this.searchResults = [];
this.searchState.complete();
}
search(): void {
if (!this.searchText) {
this.searchResults = [];
return;
}
this.currentDocumentationEntry = undefined;
this.searchState.inProgress();
this.searchResults = Array.from(
this.searchEngine
.search(
this.searchConfigurationState.generateSearchText(this.searchText),
{
// NOTE: search for limit + 1 item so we can know if there are more search results
limit: DOCUMENTATION_SEARCH_RESULTS_LIMIT + 1,
},
)
.values(),
).map((result) => new VirtualAssistantDocumentationEntry(result.item));
// check if the search results exceed the limit
if (this.searchResults.length > DOCUMENTATION_SEARCH_RESULTS_LIMIT) {
this.isOverSearchLimit = true;
this.searchResults = this.searchResults.slice(
0,
DOCUMENTATION_SEARCH_RESULTS_LIMIT,
);
} else {
this.isOverSearchLimit = false;
}
this.searchState.complete();
}
setShowSearchConfigurationMenu(val: boolean): void {
this.showSearchConfigurationMenu = val;
}
}