From 17a7697e5e12fc1103573b4254e46b2af60baa5c Mon Sep 17 00:00:00 2001 From: AleksanderSklorz <115619721+AleksanderSklorz@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:12:31 +0200 Subject: [PATCH] [ACS-8202] basic flow getting ai response for one or more selected files (#3936) * ACS-8202 Added animated icon * ACS-8202 Added search ai input * ACS-8202 Added AI search results page * ACS-8202 Allow to run knowledge retrieval on files inside library, shared, favourites and recent files * ACS-8202 Hide icon when selected more than 100 files or non text files * ACS-8202 Display notification when too many files are selected * ACS-8202 Added agents dropdown * ACS-8202 Styles for AI response * ACS-8202 Applied design changes * ACS-8202 Added query card to Knowledge retrieval page results * ACS-8202 Fixed search collapsing when opened results page * ACS-8202 Changed placeholder in input for results page, wrapping text and scrolling for results page * ACS-8202 Display snackbar with messages when conditions are not met * ACS-8202 Disallow run knowledge retrieval for libraries, leave input when click on x button * ACS-8202 Renaming files * ACS-8202 Trigger ai input by selecting agent instead of clicking on button * ACS-8202 Reverted triggering showing input by selecting option from select * ACS-8202 Display dropdown with agents by clicking on button * ACS-8202 Structural changes - services and agents button component * ACS-8202 Removed part for examples from search page * ACS-8202 Simplified html for search page * ACS-8202 Refactored html and styles for search page, translations for search page * ACS-8202 More html and styles refactoring * ACS-8202 Formatting html * ACS-8202 Removed references to angular material classes * ACS-8202 Added data automation id attributes * ACS-8202 Load agents from backend, formatting html for agents button component and adding data automation ids to that component * ACS-8202 Correction after rebase * ACS-8202 Set agent for input based on selected agent from dropdown for agents button * ACS-8202 Hide agent button for libraries pages and use translations for warnings when clicked on agents button * ACS-8202 Pass agent id to search results page * ACS-8202 Used form control instead of ngmodel for search query * ACS-8202 Moved search ai service and search ai input state to ADF * ACS-8202 Results page ts clean up * ACS-8202 Used ask and getAnswer functions from search ai service * ACS-8202 Cleaning of search ai navigation service * ACS-8202 Small clean ups * ACS-8202 Renamed sources to references * ACS-8202 Fixed asking next question from results page * ACS-8202 Added possibility to use knowledge retrieval from search results page * ACS-8202 Fixed issue with selecting the same agent after previously closing input on search results page * ACS-8202 Disallowed using knowledge retrieval on trash page * ACS-8202 Hide toggling knowledge retrieval for tasks and processes, fixed displaying ask button for favorites page * ACS-8202 Removed redundant image and function * ACS-8202 Renamed breadcrumbTemplate to header * ACS-8202 Removed redundant code, added some comments, made some fields as private * ACS-8202 Display error message on search page * ACS-8202 Accessibility changes * ACS-8202 Small correction * ACS-8202 Addressed comments * ACS-8202 Displayed correct initials * ACS-8202 Removed redundant imports * ACS-8202 Change css value * ACS-8202 Removed icon animation * ACS-8202 Removed icon animation --- .../aca-content/assets/app.extensions.json | 24 +++ projects/aca-content/assets/i18n/en.json | 28 +++ .../assets/images/colored-stars-ai.svg | 19 ++ .../assets/images/three-magic-stars-ai.svg | 19 ++ .../aca-content/src/lib/aca-content.module.ts | 3 + .../aca-content/src/lib/aca-content.routes.ts | 5 + .../favorites/favorites.component.html | 12 +- .../favorites/favorites.component.ts | 5 +- .../lib/components/files/files.component.html | 17 +- .../lib/components/files/files.component.ts | 5 +- .../agents-button.component.html | 34 ++++ .../agents-button.component.scss | 59 ++++++ .../agents-button/agents-button.component.ts | 123 +++++++++++++ .../search-ai-input-container.component.html | 17 ++ .../search-ai-input-container.component.scss | 14 ++ .../search-ai-input-container.component.ts | 81 +++++++++ .../search-ai-input.component.html | 40 +++++ .../search-ai-input.component.scss | 81 +++++++++ .../search-ai-input.component.ts | 158 ++++++++++++++++ .../search-ai-results.component.html | 107 +++++++++++ .../search-ai-results.component.scss | 124 +++++++++++++ .../search-ai-results.component.ts | 168 ++++++++++++++++++ .../recent-files/recent-files.component.html | 12 +- .../recent-files/recent-files.component.ts | 5 +- .../search-results.component.html | 14 +- .../search-results.component.scss | 13 +- .../search-results.component.ts | 4 +- .../shared-files/shared-files.component.html | 12 +- .../shared-files/shared-files.component.ts | 5 +- .../services/search-ai-navigation.service.ts | 48 +++++ .../src/lib/store/app-store.module.ts | 4 +- projects/aca-content/src/lib/store/effects.ts | 1 + .../lib/store/effects/search-ai.effects.ts | 64 +++++++ .../aca-content/src/lib/ui/application.scss | 8 + .../src/lib/ui/variables/variables.scss | 6 +- projects/aca-shared/rules/src/app.rules.ts | 7 + .../document-base-page.component.ts | 18 +- .../store/src/actions/search-ai.actions.ts | 42 +++++ .../src/models/ai-search-by-term-payload.ts | 28 +++ projects/aca-shared/store/src/public-api.ts | 2 + 40 files changed, 1411 insertions(+), 25 deletions(-) create mode 100644 projects/aca-content/assets/images/colored-stars-ai.svg create mode 100644 projects/aca-content/assets/images/three-magic-stars-ai.svg create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.html create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.scss create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.ts create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.html create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.scss create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.ts create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.html create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.scss create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.ts create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.html create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.scss create mode 100644 projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.ts create mode 100644 projects/aca-content/src/lib/services/search-ai-navigation.service.ts create mode 100644 projects/aca-content/src/lib/store/effects/search-ai.effects.ts create mode 100644 projects/aca-shared/store/src/actions/search-ai.actions.ts create mode 100644 projects/aca-shared/store/src/models/ai-search-by-term-payload.ts diff --git a/projects/aca-content/assets/app.extensions.json b/projects/aca-content/assets/app.extensions.json index a21a0d2bb..23b3ade32 100644 --- a/projects/aca-content/assets/app.extensions.json +++ b/projects/aca-content/assets/app.extensions.json @@ -136,6 +136,14 @@ { "id": "adf:move_file", "value": "./assets/images/adf-move-file-24px.svg" + }, + { + "id": "adf:three_magic_stars_ai", + "value": "./assets/images/three-magic-stars-ai.svg" + }, + { + "id": "adf:colored-stars-ai", + "value": "./assets/images/colored-stars-ai.svg" } ], "create": [ @@ -692,6 +700,22 @@ } } ] + }, + { + "id": "app.toolbar.ai.search", + "order": 0, + "title": "KNOWLEDGE_RETRIEVAL.SEARCH.AGENTS_BUTTON.TITLE", + "component": "app.toolbar.ai.agents-button", + "type": "custom", + "rules": { + "visible": "app.selection.displayedKnowledgeRetrievalButton" + }, + "actions": { + "click": "app.action.toggle-ai-search-input.execute" + }, + "data": { + "trigger": "TOGGLE_AI_SEARCH_INPUT" + } } ], "contextMenu": [ diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json index de1acbfe5..ddd9e5b57 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -603,5 +603,33 @@ "BOOKS-24PX": "file library", "BASELINE-LOCK-24PX": "locked file" } + }, + "KNOWLEDGE_RETRIEVAL": { + "SEARCH": { + "RESULTS_PAGE": { + "QUERY_INPUT_PLACEHOLDER": "Would you like to ask anything else?", + "REFERENCED_DOCUMENTS_HEADER": "Referenced documents", + "REGENERATION_BUTTON_LABEL": "Regenerate", + "COPY_BUTTON_LABEL": "Copy", + "LIKE_BUTTON_LABEL": "Like", + "DISLIKE_BUTTON_LABEL": "Dislike", + "COPY_MESSAGE": "Copied response to clipboard" + }, + "AGENTS_BUTTON": { + "LABEL": "Ask Agent", + "TITLE": "Knowledge Retrieval" + }, + "SEARCH_INPUT": { + "ASK_BUTTON_LABEL": "Ask", + "DEFAULT_PLACEHOLDER": "Please ask your question with as much detail as possible...", + "HIDE_INPUT": "Hide input" + }, + "ERRORS": { + "AGENTS_FETCHING": "Error while fetching agents.", + "LOADING_ERROR": "Hmm... something seems to have gone wrong.", + "PAGE_NOT_AVAILABLE_ERROR": "Page is not available for these conditions." + + } + } } } diff --git a/projects/aca-content/assets/images/colored-stars-ai.svg b/projects/aca-content/assets/images/colored-stars-ai.svg new file mode 100644 index 000000000..bf397f933 --- /dev/null +++ b/projects/aca-content/assets/images/colored-stars-ai.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/projects/aca-content/assets/images/three-magic-stars-ai.svg b/projects/aca-content/assets/images/three-magic-stars-ai.svg new file mode 100644 index 000000000..992394ec0 --- /dev/null +++ b/projects/aca-content/assets/images/three-magic-stars-ai.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/projects/aca-content/src/lib/aca-content.module.ts b/projects/aca-content/src/lib/aca-content.module.ts index 033ba8880..23ca3a248 100644 --- a/projects/aca-content/src/lib/aca-content.module.ts +++ b/projects/aca-content/src/lib/aca-content.module.ts @@ -76,6 +76,7 @@ import { UserMenuComponent } from './components/sidenav/user-menu/user-menu.comp import { ContextMenuComponent } from './components/context-menu/context-menu.component'; import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; +import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component'; @NgModule({ imports: [ @@ -137,6 +138,7 @@ export class ContentServiceExtensionModule { 'app.toolbar.toggleFavorite': ToggleFavoriteComponent, 'app.toolbar.toggleFavoriteLibrary': ToggleFavoriteLibraryComponent, 'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryButtonComponent, + 'app.toolbar.ai.agents-button': AgentsButtonComponent, 'app.menu.toggleJoinLibrary': ToggleJoinLibraryMenuComponent, 'app.shared-link.toggleSharedLink': ToggleSharedComponent, 'app.columns.name': CustomNameColumnComponent, @@ -196,6 +198,7 @@ export class ContentServiceExtensionModule { 'app.selection.hasNoLibraryRole': rules.hasNoLibraryRole, 'app.selection.folder': rules.hasFolderSelected, 'app.selection.folder.canUpdate': rules.canUpdateSelectedFolder, + 'app.selection.displayedKnowledgeRetrievalButton': rules.canDisplayKnowledgeRetrievalButton, 'app.navigation.folder.canCreate': rules.canCreateFolder, 'app.navigation.folder.canUpload': rules.canUpload, diff --git a/projects/aca-content/src/lib/aca-content.routes.ts b/projects/aca-content/src/lib/aca-content.routes.ts index 85da0484e..23fd539c4 100644 --- a/projects/aca-content/src/lib/aca-content.routes.ts +++ b/projects/aca-content/src/lib/aca-content.routes.ts @@ -40,6 +40,7 @@ import { Route } from '@angular/router'; import { SharedLinkViewComponent } from './components/shared-link-view/shared-link-view.component'; import { TrashcanComponent } from './components/trashcan/trashcan.component'; import { ShellLayoutComponent } from '@alfresco/adf-core/shell'; +import { SearchAiResultsComponent } from './components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component'; export const CONTENT_ROUTES: ExtensionRoute[] = [ { @@ -507,6 +508,10 @@ export const CONTENT_LAYOUT_ROUTES: Route = { } ] }, + { + path: 'knowledge-retrieval', + component: SearchAiResultsComponent + }, { path: '**', component: GenericErrorComponent diff --git a/projects/aca-content/src/lib/components/favorites/favorites.component.html b/projects/aca-content/src/lib/components/favorites/favorites.component.html index c95652f54..fa524951c 100644 --- a/projects/aca-content/src/lib/components/favorites/favorites.component.html +++ b/projects/aca-content/src/lib/components/favorites/favorites.component.html @@ -1,7 +1,15 @@
-

{{ 'APP.BROWSE.FAVORITES.TITLE' | translate }}

- + + + +
+

{{ 'APP.BROWSE.FAVORITES.TITLE' | translate }}

+ +
+
diff --git a/projects/aca-content/src/lib/components/favorites/favorites.component.ts b/projects/aca-content/src/lib/components/favorites/favorites.component.ts index c0be53d42..468dd8c3e 100644 --- a/projects/aca-content/src/lib/components/favorites/favorites.component.ts +++ b/projects/aca-content/src/lib/components/favorites/favorites.component.ts @@ -40,6 +40,7 @@ import { DocumentListModule } from '@alfresco/adf-content-services'; import { DataTableModule, EmptyContentComponent, PaginationComponent } from '@alfresco/adf-core'; import { DocumentListDirective } from '../../directives/document-list.directive'; import { TranslateModule } from '@ngx-translate/core'; +import { SearchAiInputContainerComponent } from '../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -55,11 +56,13 @@ import { TranslateModule } from '@ngx-translate/core'; PageLayoutComponent, TranslateModule, ToolbarComponent, + SearchAiInputContainerComponent, EmptyContentComponent, DynamicColumnComponent ], templateUrl: './favorites.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, + selector: 'aca-favorites' }) export class FavoritesComponent extends PageComponent implements OnInit { columns: DocumentListPresetRef[] = []; diff --git a/projects/aca-content/src/lib/components/files/files.component.html b/projects/aca-content/src/lib/components/files/files.component.html index d3d377852..deced74e8 100644 --- a/projects/aca-content/src/lib/components/files/files.component.html +++ b/projects/aca-content/src/lib/components/files/files.component.html @@ -1,7 +1,20 @@
- - + + + +
+ + + +
+
diff --git a/projects/aca-content/src/lib/components/files/files.component.ts b/projects/aca-content/src/lib/components/files/files.component.ts index 71be16e0d..9c660f34d 100644 --- a/projects/aca-content/src/lib/components/files/files.component.ts +++ b/projects/aca-content/src/lib/components/files/files.component.ts @@ -45,6 +45,7 @@ import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { DocumentListDirective } from '../../directives/document-list.directive'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { SearchAiInputContainerComponent } from '../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -64,10 +65,12 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; PaginationDirective, PageLayoutComponent, ToolbarComponent, + SearchAiInputContainerComponent, DynamicColumnComponent ], templateUrl: './files.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, + selector: 'aca-files' }) export class FilesComponent extends PageComponent implements OnInit, OnDestroy { isValidPath = true; diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.html b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.html new file mode 100644 index 000000000..ab81e2b75 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.html @@ -0,0 +1,34 @@ + + + + + +
+ + {{ agent.name }} +
+
+
+
+
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.scss b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.scss new file mode 100644 index 000000000..327bf2220 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.scss @@ -0,0 +1,59 @@ +aca-agents-button.aca-agents-button { + height: 32px; + display: block; + + button { + &.aca-agents-menu-button { + &.aca-agents-button-menu-trigger { + height: auto; + cursor: pointer; + border: none; + background: transparent; + width: max-content; + padding: 0 4px 0 0; + } + + .aca-agents-button-icon { + display: flex; + align-self: baseline; + + svg { + height: 32px; + width: 32px; + position: absolute; + margin-left: -21px; + } + } + } + } +} + +.aca-agents-button-menu { + padding-top: 2px; + padding-bottom: 1px; + + .aca-agents-button-menu-list { + margin-left: -6px; + padding-top: 0; + + &-agent { + height: 40px; + + &:not(:last-child) { + margin-bottom: 2px; + } + + &-content { + display: flex; + align-items: baseline; + } + + adf-avatar { + margin-right: 12px; + margin-bottom: 2px; + padding-left: 1px; + padding-top: 1px; + } + } + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.ts new file mode 100644 index 000000000..289821637 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.ts @@ -0,0 +1,123 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { Store } from '@ngrx/store'; +import { AppStore, getAppSelection } from '@alfresco/aca-shared/store'; +import { AvatarComponent, IconComponent, NotificationService } from '@alfresco/adf-core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatListModule, MatSelectionListChange } from '@angular/material/list'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Agent } from '@alfresco/js-api'; +import { AgentService, SearchAiService } from '@alfresco/adf-content-services'; + +@Component({ + standalone: true, + imports: [CommonModule, MatMenuModule, MatListModule, TranslateModule, AvatarComponent, IconComponent], + selector: 'aca-agents-button', + templateUrl: './agents-button.component.html', + styleUrls: ['./agents-button.component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'aca-agents-button' } +}) +export class AgentsButtonComponent implements OnInit, OnDestroy { + @Input() + data: { trigger: string }; + + private selectedNodesState: SelectionState; + private _agents: Agent[] = []; + private onDestroy$ = new Subject(); + private _disabled = true; + private _initialsByAgentId: { [key: string]: string } = {}; + + get agents(): Agent[] { + return this._agents; + } + + get disabled(): boolean { + return this._disabled; + } + + get initialsByAgentId(): { [key: string]: string } { + return this._initialsByAgentId; + } + + constructor( + private store: Store, + private notificationService: NotificationService, + private searchAiService: SearchAiService, + private translateService: TranslateService, + private agentService: AgentService + ) {} + + ngOnInit(): void { + this.store + .select(getAppSelection) + .pipe(takeUntil(this.onDestroy$)) + .subscribe((selection) => { + this.selectedNodesState = selection; + }); + this.agentService + .getAgents() + .pipe(takeUntil(this.onDestroy$)) + .subscribe( + (paging) => { + this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry); + if (this.agents.length) { + this._initialsByAgentId = this.agents.reduce((initials, agent) => { + const words = agent.name.split(' ').filter((word) => !word.match(/[^a-zA-Z]+/g)); + initials[agent.id] = `${words[0][0]}${words[1][0] || ''}`; + return initials; + }, {}); + } + }, + () => this.notificationService.showError(this.translateService.instant('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING')) + ); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + onClick(): void { + const error = this.searchAiService.checkSearchAvailability(this.selectedNodesState); + if (error) { + this.notificationService.showInfo(error); + } + this._disabled = !!error; + } + + onAgentSelection(change: MatSelectionListChange): void { + this.store.dispatch({ + type: this.data.trigger, + agentId: change.options[0].value.id + }); + change.source.deselectAll(); + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.html b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.html new file mode 100644 index 000000000..bd784d5b5 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.html @@ -0,0 +1,17 @@ + + + + + diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.scss b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.scss new file mode 100644 index 000000000..1efd2eaeb --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.scss @@ -0,0 +1,14 @@ +aca-search-ai-input-container { + display: flex; + flex-direction: row; + flex: 1; + align-items: center; + width: 100%; + + .aca-search-ai-input-container-divider { + height: 24px; + margin-left: 30px; + margin-right: 7px; + background: var(--adf-theme-foreground-text-color-025); + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.ts new file mode 100644 index 000000000..af5fa2a16 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.ts @@ -0,0 +1,81 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { SearchAiInputComponent } from '../search-ai-input/search-ai-input.component'; +import { MatDividerModule } from '@angular/material/divider'; +import { SearchAiNavigationService } from '../../../../services/search-ai-navigation.service'; +import { NavigationStart, Router } from '@angular/router'; +import { filter, takeUntil } from 'rxjs/operators'; +import { SearchAiService } from '@alfresco/adf-content-services'; +import { TranslateModule } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; + +@Component({ + standalone: true, + imports: [SearchAiInputComponent, MatIconModule, MatDividerModule, MatButtonModule, TranslateModule], + selector: 'aca-search-ai-input-container', + templateUrl: './search-ai-input-container.component.html', + styleUrls: ['./search-ai-input-container.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchAiInputContainerComponent implements OnInit, OnDestroy { + @Input() + placeholder = 'KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.DEFAULT_PLACEHOLDER'; + @Input() + agentId: string; + @Input() + useStoredNodes: boolean; + + private onDestroy$ = new Subject(); + + constructor(private searchAiService: SearchAiService, private searchNavigationService: SearchAiNavigationService, private router: Router) {} + + ngOnInit(): void { + this.router.events + .pipe( + filter((event) => event instanceof NavigationStart), + takeUntil(this.onDestroy$) + ) + .subscribe(() => this.hideSearchInput()); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + hideSearchInput(): void { + this.searchAiService.updateSearchAiInputState({ + active: false + }); + } + + leaveSearchInput(): void { + this.searchNavigationService.navigateToPreviousRoute(); + this.hideSearchInput(); + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.html b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.html new file mode 100644 index 000000000..bad899e11 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.html @@ -0,0 +1,40 @@ + + + + + {{ agentControl.value.name }} + + +
+ + {{ agent.name }} +
+
+
+ + diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.scss b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.scss new file mode 100644 index 000000000..48c3d7483 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.scss @@ -0,0 +1,81 @@ +aca-search-ai-input { + width: 100%; + display: flex; + align-items: center; + + .aca-search-ai-input-text { + flex: 1; + font-size: 20px; + margin-right: 167px; + border: none; + outline: none; + + &:focus { + &::placeholder { + color: var(--theme-primary-color); + } + } + } + + .aca-search-ai-asking-button { + display: flex; + align-items: center; + padding-left: 5px; + padding-right: 12px; + height: 32px; + border-radius: 6px; + + adf-icon { + margin-bottom: 3px; + margin-right: 4px; + } + } + + .aca-search-ai-input-agent-select { + width: 149px; + height: 35px; + align-content: center; + border-radius: 16px; + padding-left: 3px; + padding-right: 10px; + background-color: var(--theme-grey-text-background-color); + color: var(--theme-text-light-color); + font-size: 15px; + margin-right: 26px; + + &:focus { + outline: -webkit-focus-ring-color auto 1px; + } + + &-displayed-value { + display: flex; + align-items: baseline; + } + + adf-avatar { + margin-left: 2px; + margin-right: 6px; + padding-top: 1px; + padding-bottom: 3px; + } + } +} + +.aca-search-ai-input-agent-select-options { + margin-top: 48px; + + .aca-search-ai-input-agent-select-options-option { + padding-left: 11px; + padding-right: 11px; + + &-content { + display: flex; + align-items: baseline; + } + + adf-avatar { + margin-right: 12px; + padding-left: 1px; + } + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.ts new file mode 100644 index 000000000..54e074cb4 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.ts @@ -0,0 +1,158 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { A11yModule } from '@angular/cdk/a11y'; +import { AvatarComponent, IconComponent, NotificationService, UserPreferencesService } from '@alfresco/adf-core'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { AiSearchByTermPayload, AppStore, getAppSelection, SearchByTermAiAction } from '@alfresco/aca-shared/store'; +import { takeUntil } from 'rxjs/operators'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { MatSelectModule } from '@angular/material/select'; +import { Agent } from '@alfresco/js-api'; +import { AgentService, SearchAiService } from '@alfresco/adf-content-services'; + +@Component({ + standalone: true, + imports: [ + CommonModule, + TranslateModule, + MatButtonModule, + MatIconModule, + MatFormFieldModule, + MatInputModule, + A11yModule, + FormsModule, + ReactiveFormsModule, + MatSelectModule, + IconComponent, + AvatarComponent + ], + selector: 'aca-search-ai-input', + templateUrl: './search-ai-input.component.html', + styleUrls: ['./search-ai-input.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchAiInputComponent implements OnInit, OnDestroy { + @Input() + placeholder: string; + @Input() + agentId: string; + @Input() + useStoredNodes: boolean; + + @Output() + searchSubmitted = new EventEmitter(); + + private readonly storedNodesKey = 'knowledgeRetrievalNodes'; + + private _agentControl = new FormControl(null); + private _agents: Agent[] = []; + private onDestroy$ = new Subject(); + private selectedNodesState: SelectionState; + private _queryControl = new FormControl(''); + private _initialsByAgentId: { [key: string]: string } = {}; + + get agentControl(): FormControl { + return this._agentControl; + } + + get agents(): Agent[] { + return this._agents; + } + + get queryControl(): FormControl { + return this._queryControl; + } + + get initialsByAgentId(): { [key: string]: string } { + return this._initialsByAgentId; + } + + constructor( + private store: Store, + private searchAiService: SearchAiService, + private notificationService: NotificationService, + private agentService: AgentService, + private translateService: TranslateService, + private userPreferencesService: UserPreferencesService + ) {} + + ngOnInit(): void { + if (!this.useStoredNodes) { + this.store + .select(getAppSelection) + .pipe(takeUntil(this.onDestroy$)) + .subscribe((selection) => { + this.selectedNodesState = selection; + }); + } else { + this.selectedNodesState = JSON.parse(this.userPreferencesService.get(this.storedNodesKey)); + } + this.agentService + .getAgents() + .pipe(takeUntil(this.onDestroy$)) + .subscribe( + (paging) => { + this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry); + this.agentControl.setValue(this.agents.find((agent) => agent.id === this.agentId)); + this._initialsByAgentId = this.agents.reduce((initials, agent) => { + const words = agent.name.split(' ').filter((word) => !word.match(/[^a-zA-Z]+/g)); + initials[agent.id] = `${words[0][0]}${words[1][0] || ''}`; + return initials; + }, {}); + }, + () => this.notificationService.showError(this.translateService.instant('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING')) + ); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + onSearchSubmit(): void { + const error = this.searchAiService.checkSearchAvailability(this.selectedNodesState); + if (error) { + this.notificationService.showInfo(error); + } else { + const payload: AiSearchByTermPayload = { + searchTerm: this.queryControl.value, + agentId: this.agentControl.value.id + }; + this.userPreferencesService.set(this.storedNodesKey, JSON.stringify(this.selectedNodesState)); + this.store.dispatch(new SearchByTermAiAction(payload)); + this.queryControl.reset(); + this.searchSubmitted.emit(); + } + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.html b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.html new file mode 100644 index 000000000..177ff34ee --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.html @@ -0,0 +1,107 @@ + + + +
+
+
+ {{ searchQuery }} +
+
+ +
+
+ {{ queryAnswer?.answer }} +
+ + + + + + +
+

+ {{ 'KNOWLEDGE_RETRIEVAL.SEARCH.RESULTS_PAGE.REFERENCED_DOCUMENTS_HEADER' | translate }} +

+
+
+ + + +
+ {{ node.name }} +
+
+
+
+
+
+
+ {{ 'KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.LOADING_ERROR' | translate }} + +
+
+
+
+
+ + +
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.scss b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.scss new file mode 100644 index 000000000..c95a64486 --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.scss @@ -0,0 +1,124 @@ +.aca-search-ai-results { + aca-page-layout { + .aca-page-layout-content { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + background-color: white; + border-top: 1px solid var(--theme-grey-background-color); + padding-top: 28px; + + .aca-search-ai-results-container { + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + padding-right: 24%; + padding-left: 24%; + min-width: 51%; + + &-query { + border-radius: 12px; + padding: 20px 15px 19px; + background: var(--theme-card-background-grey-color); + } + } + + .aca-search-ai-response-container { + display: flex; + flex-direction: column; + border: 1px solid var(--adf-card-view-border-color); + border-radius: 12px; + margin: 16px 0 75px; + padding: 14px 40px 36px 35px; + + &-error { + border-color: var(--adf-error-color); + padding: 21px 19px 27px 18px; + + &-message { + display: flex; + justify-content: space-between; + align-items: center; + + &-regeneration-button { + background-color: var(--adf-secondary-button-background); + } + } + } + + &-body { + width: 100%; + + &-response { + margin-bottom: 17px; + padding-left: 6px; + padding-right: 5px; + overflow-wrap: break-word; + + &-action { + width: max-content; + + mat-icon { + font-size: 17.25px; + } + + &-regeneration { + margin-left: 2px; + margin-right: 2px; + } + + &-thumb-down { + margin-left: 4px; + } + } + } + + &-divider { + margin-top: 9px; + } + + &-references-container { + padding-right: 8px; + padding-left: 8px; + + &-header { + margin-top: 8px; + color: var(--theme-text-light-color); + font-weight: 400; + margin-bottom: 3px; + } + + &-documents { + padding-right: 5px; + padding-top: 5px; + margin-left: -2px; + display: flex; + gap: 21px; + + &-document { + display: flex; + flex-direction: row; + padding-top: 7px; + padding-bottom: 7px; + + &-icon { + padding-right: 11px; + } + + &-name { + display: flex; + flex-direction: column; + width: 100%; + justify-content: center; + } + } + } + } + } + } + } + } +} diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.ts new file mode 100644 index 000000000..e32e8295d --- /dev/null +++ b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.ts @@ -0,0 +1,168 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { PageComponent, PageLayoutComponent, ToolbarActionComponent, ToolbarComponent } from '@alfresco/aca-shared'; +import { finalize, switchMap, takeUntil } from 'rxjs/operators'; +import { ClipboardService, EmptyContentComponent, ThumbnailService, ToolbarModule, UserPreferencesService } from '@alfresco/adf-core'; +import { AiAnswer, Node } from '@alfresco/js-api'; +import { CommonModule } from '@angular/common'; +import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { NodesApiService } from '@alfresco/adf-content-services'; +import { forkJoin } from 'rxjs'; +import { SelectionState } from '@alfresco/adf-extensions'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatListModule } from '@angular/material/list'; + +@Component({ + standalone: true, + imports: [ + CommonModule, + PageLayoutComponent, + ToolbarActionComponent, + ToolbarModule, + ToolbarComponent, + SearchAiInputContainerComponent, + TranslateModule, + MatIconModule, + MatButtonModule, + MatListModule, + EmptyContentComponent + ], + selector: 'aca-search-ai-results', + templateUrl: './search-ai-results.component.html', + styleUrls: ['./search-ai-results.component.scss'], + encapsulation: ViewEncapsulation.None, + host: { class: 'aca-search-ai-results' } +}) +export class SearchAiResultsComponent extends PageComponent implements OnInit, OnDestroy { + private _agentId: string; + private _hasAnsweringError = false; + private _hasError = false; + private _loading = true; + private _mimeTypeIconsByNodeId: { [key: string]: string } = {}; + private _nodes: Node[] = []; + private selectedNodesState: SelectionState; + private _searchQuery = ''; + private _queryAnswer: AiAnswer; + + get agentId(): string { + return this._agentId; + } + + get hasAnsweringError(): boolean { + return this._hasAnsweringError; + } + + get hasError(): boolean { + return this._hasError; + } + + get loading(): boolean { + return this._loading; + } + + get mimeTypeIconsByNodeId(): { [key: string]: string } { + return this._mimeTypeIconsByNodeId; + } + + get nodes(): Node[] { + return this._nodes; + } + + get queryAnswer(): AiAnswer { + return this._queryAnswer; + } + + get searchQuery(): string { + return this._searchQuery; + } + + constructor( + private route: ActivatedRoute, + private clipboardService: ClipboardService, + private thumbnailService: ThumbnailService, + private nodesApiService: NodesApiService, + private userPreferencesService: UserPreferencesService, + private translateService: TranslateService + ) { + super(); + } + + ngOnInit(): void { + this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => { + this._agentId = params.agentId; + this._searchQuery = params.query ? decodeURIComponent(params.query) : ''; + this.selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes')); + if (!this.searchQuery || !this.selectedNodesState?.nodes?.length || !this.agentId) { + this._hasError = true; + return; + } + this.performAiSearch(); + }); + super.ngOnInit(); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + copyResponseToClipboard(): void { + this.clipboardService.copyContentToClipboard( + this.queryAnswer.answer, + this.translateService.instant('KNOWLEDGE_RETRIEVAL.SEARCH.RESULTS_PAGE.COPY_MESSAGE') + ); + } + + performAiSearch(): void { + this._loading = true; + this.searchAiService + .ask({ + question: this.searchQuery, + nodeIds: this.selectedNodesState.nodes.map((node) => node.entry.id) + }) + .pipe( + switchMap((response) => this.searchAiService.getAnswer(response.questionId)), + switchMap((response) => { + this._queryAnswer = response.list.entries[0].entry; + return forkJoin(this.queryAnswer.references.map((reference) => this.nodesApiService.getNode(reference.referenceId))); + }), + finalize(() => (this._loading = false)), + takeUntil(this.onDestroy$) + ) + .subscribe( + (nodes) => { + nodes.forEach((node) => { + this._mimeTypeIconsByNodeId[node.id] = this.thumbnailService.getMimeTypeIcon(node.content?.mimeType); + }); + this._nodes = nodes; + }, + () => (this._hasAnsweringError = true) + ); + } +} diff --git a/projects/aca-content/src/lib/components/recent-files/recent-files.component.html b/projects/aca-content/src/lib/components/recent-files/recent-files.component.html index ef06f8ed9..2fac61f3a 100644 --- a/projects/aca-content/src/lib/components/recent-files/recent-files.component.html +++ b/projects/aca-content/src/lib/components/recent-files/recent-files.component.html @@ -1,7 +1,15 @@
-

{{ 'APP.BROWSE.RECENT.TITLE' | translate }}

- + + + +
+

{{ 'APP.BROWSE.RECENT.TITLE' | translate }}

+ +
+
diff --git a/projects/aca-content/src/lib/components/recent-files/recent-files.component.ts b/projects/aca-content/src/lib/components/recent-files/recent-files.component.ts index 8db29b52c..c7ef0377a 100644 --- a/projects/aca-content/src/lib/components/recent-files/recent-files.component.ts +++ b/projects/aca-content/src/lib/components/recent-files/recent-files.component.ts @@ -39,6 +39,7 @@ import { DocumentListModule } from '@alfresco/adf-content-services'; import { DataTableModule, EmptyContentComponent, PaginationComponent } from '@alfresco/adf-core'; import { DocumentListDirective } from '../../directives/document-list.directive'; import { TranslateModule } from '@ngx-translate/core'; +import { SearchAiInputContainerComponent } from '../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -54,11 +55,13 @@ import { TranslateModule } from '@ngx-translate/core'; PageLayoutComponent, TranslateModule, ToolbarComponent, + SearchAiInputContainerComponent, EmptyContentComponent, DynamicColumnComponent ], templateUrl: './recent-files.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, + selector: 'aca-recent-files' }) export class RecentFilesComponent extends PageComponent implements OnInit { columns: DocumentListPresetRef[] = []; diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html index d642335ff..a1da0b2c2 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html @@ -1,8 +1,14 @@ - +
- -
- + + +
+ +
+ +
diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.scss b/projects/aca-content/src/lib/components/search/search-results/search-results.component.scss index 2bd478ccb..ea0bbb24f 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.scss +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.scss @@ -1,6 +1,13 @@ @import '../../../ui/mixins'; aca-search-results { + .aca-search-results-active-search-ai-input { + .aca-header-container, + .adf-search-results__content-header.aca-content { + display: none; + } + } + .aca-search-toolbar-spacer { width: 100%; } @@ -45,10 +52,10 @@ aca-search-results { color: var(--theme-search-chip-icon-color); } - .adf-search-filter-chip-icon { - color: var(--theme-search-chip-icon-color); + .adf-search-filter-chip-icon { + color: var(--theme-search-chip-icon-color); + } } - } .adf-search-filter-placeholder { color: var(--theme-selected-text-color); diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts index 981e97b49..5bfdddc29 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts @@ -60,6 +60,7 @@ import { TagsColumnComponent } from '../../dl-custom-components/tags-column/tags import { MatIconModule } from '@angular/material/icon'; import { SearchResultsRowComponent } from '../search-results-row/search-results-row.component'; import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions'; +import { SearchAiInputContainerComponent } from '../../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -87,7 +88,8 @@ import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-ext PageLayoutComponent, ToolbarComponent, AlfrescoViewerComponent, - DynamicColumnComponent + DynamicColumnComponent, + SearchAiInputContainerComponent ], selector: 'aca-search-results', templateUrl: './search-results.component.html', diff --git a/projects/aca-content/src/lib/components/shared-files/shared-files.component.html b/projects/aca-content/src/lib/components/shared-files/shared-files.component.html index f896aacd7..d54f1485f 100644 --- a/projects/aca-content/src/lib/components/shared-files/shared-files.component.html +++ b/projects/aca-content/src/lib/components/shared-files/shared-files.component.html @@ -1,7 +1,15 @@
-

{{ 'APP.BROWSE.SHARED.TITLE' | translate }}

- + + + +
+

{{ 'APP.BROWSE.SHARED.TITLE' | translate }}

+ +
+
diff --git a/projects/aca-content/src/lib/components/shared-files/shared-files.component.ts b/projects/aca-content/src/lib/components/shared-files/shared-files.component.ts index 3c8793aad..d84d0e36f 100644 --- a/projects/aca-content/src/lib/components/shared-files/shared-files.component.ts +++ b/projects/aca-content/src/lib/components/shared-files/shared-files.component.ts @@ -40,6 +40,7 @@ import { DocumentListModule } from '@alfresco/adf-content-services'; import { DataTableModule, EmptyContentComponent, PaginationComponent } from '@alfresco/adf-core'; import { DocumentListDirective } from '../../directives/document-list.directive'; import { TranslateModule } from '@ngx-translate/core'; +import { SearchAiInputContainerComponent } from '../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -55,11 +56,13 @@ import { TranslateModule } from '@ngx-translate/core'; PageLayoutComponent, TranslateModule, ToolbarComponent, + SearchAiInputContainerComponent, EmptyContentComponent, DynamicColumnComponent ], templateUrl: './shared-files.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, + selector: 'aca-shared-files' }) export class SharedFilesComponent extends PageComponent implements OnInit { columns: DocumentListPresetRef[] = []; diff --git a/projects/aca-content/src/lib/services/search-ai-navigation.service.ts b/projects/aca-content/src/lib/services/search-ai-navigation.service.ts new file mode 100644 index 000000000..5fc3cb197 --- /dev/null +++ b/projects/aca-content/src/lib/services/search-ai-navigation.service.ts @@ -0,0 +1,48 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Injectable } from '@angular/core'; +import { Params, Router } from '@angular/router'; + +@Injectable({ providedIn: 'root' }) +export class SearchAiNavigationService { + private readonly knowledgeRetrievalRoute = '/knowledge-retrieval'; + + private previousRoute = ''; + + constructor(private router: Router) {} + + navigateToPreviousRoute(): void { + if (this.router.url.includes(this.knowledgeRetrievalRoute)) { + void this.router.navigateByUrl(this.previousRoute || '/personal-files'); + } + } + + navigateToSearchAi(queryParams: Params): void { + if (!this.router.url.includes(this.knowledgeRetrievalRoute)) { + this.previousRoute = this.router.url; + } + void this.router.navigate([this.knowledgeRetrievalRoute], { queryParams: queryParams }); + } +} diff --git a/projects/aca-content/src/lib/store/app-store.module.ts b/projects/aca-content/src/lib/store/app-store.module.ts index c2c84a142..76c08a957 100644 --- a/projects/aca-content/src/lib/store/app-store.module.ts +++ b/projects/aca-content/src/lib/store/app-store.module.ts @@ -41,6 +41,7 @@ import { ContextMenuEffects } from './effects'; import { INITIAL_STATE } from './initial-state'; +import { SearchAiEffects } from './effects/search-ai.effects'; @NgModule({ imports: [ @@ -69,7 +70,8 @@ import { INITIAL_STATE } from './initial-state'; UploadEffects, FavoriteEffects, TemplateEffects, - ContextMenuEffects + ContextMenuEffects, + SearchAiEffects ]) ] }) diff --git a/projects/aca-content/src/lib/store/effects.ts b/projects/aca-content/src/lib/store/effects.ts index f8eb84022..423fed8d0 100644 --- a/projects/aca-content/src/lib/store/effects.ts +++ b/projects/aca-content/src/lib/store/effects.ts @@ -33,3 +33,4 @@ export * from './effects/upload.effects'; export * from './effects/upload.effects'; export * from './effects/template.effects'; export * from './effects/contextmenu.effects'; +export * from './effects/search-ai.effects'; diff --git a/projects/aca-content/src/lib/store/effects/search-ai.effects.ts b/projects/aca-content/src/lib/store/effects/search-ai.effects.ts new file mode 100644 index 000000000..6de4a8d54 --- /dev/null +++ b/projects/aca-content/src/lib/store/effects/search-ai.effects.ts @@ -0,0 +1,64 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { SearchAiActionTypes, SearchByTermAiAction, ToggleAISearchInput } from '@alfresco/aca-shared/store'; +import { map } from 'rxjs/operators'; +import { SearchAiNavigationService } from '../../services/search-ai-navigation.service'; +import { SearchAiService } from '@alfresco/adf-content-services'; + +@Injectable() +export class SearchAiEffects { + constructor(private actions$: Actions, private searchNavigationService: SearchAiNavigationService, private searchAiService: SearchAiService) {} + + searchByTerm$ = createEffect( + () => + this.actions$.pipe( + ofType(SearchAiActionTypes.SearchByTermAi), + map((action) => { + const queryParams = { + query: encodeURIComponent(action.payload.searchTerm), + agentId: action.payload.agentId + }; + this.searchNavigationService.navigateToSearchAi(queryParams); + }) + ), + { dispatch: false } + ); + + toggleAISearchInput$ = createEffect( + () => + this.actions$.pipe( + ofType(SearchAiActionTypes.ToggleAiSearchInput), + map((action) => + this.searchAiService.updateSearchAiInputState({ + active: true, + selectedAgentId: action.agentId + }) + ) + ), + { dispatch: false } + ); +} diff --git a/projects/aca-content/src/lib/ui/application.scss b/projects/aca-content/src/lib/ui/application.scss index 0762b52a4..31eac5710 100644 --- a/projects/aca-content/src/lib/ui/application.scss +++ b/projects/aca-content/src/lib/ui/application.scss @@ -66,3 +66,11 @@ ng-component { color: var(--adf-theme-foreground-text-color-087); width: 100%; } + +.aca-header-container { + display: flex; + flex-direction: row; + flex: 1; + align-items: center; + width: 100%; +} diff --git a/projects/aca-content/src/lib/ui/variables/variables.scss b/projects/aca-content/src/lib/ui/variables/variables.scss index aa2e77f54..1d3c940b9 100644 --- a/projects/aca-content/src/lib/ui/variables/variables.scss +++ b/projects/aca-content/src/lib/ui/variables/variables.scss @@ -42,6 +42,8 @@ $search-chip-icon-color: #757575; $disabled-chip-background-color: #f5f5f5; $contrast-gray: mat.get-color-from-palette($foreground, 'secondary-tex'); $search-highlight-background-color: #ffd180; +$text-light-color: rgba(33, 35, 40, 0.7); +$card-background-grey-color: rgb(248, 248, 248); // CSS Variables $defaults: ( @@ -88,7 +90,9 @@ $defaults: ( --theme-search-chip-icon-color: $search-chip-icon-color, --theme-disabled-chip-background-color: $disabled-chip-background-color, --theme-secondary-text: $secondary-text, - --theme-search-highlight-background-color: $search-highlight-background-color + --theme-search-highlight-background-color: $search-highlight-background-color, + --theme-text-light-color: $text-light-color, + --theme-card-background-grey-color: $card-background-grey-color ); // propagates SCSS variables into the CSS variables scope diff --git a/projects/aca-shared/rules/src/app.rules.ts b/projects/aca-shared/rules/src/app.rules.ts index 60eeacd8b..8af63cb84 100644 --- a/projects/aca-shared/rules/src/app.rules.ts +++ b/projects/aca-shared/rules/src/app.rules.ts @@ -631,3 +631,10 @@ export function isSmartFolder(context: RuleContext): boolean { export const areTagsEnabled = (context: AcaRuleContext): boolean => context.appConfig.get('plugins.tagsEnabled', true); export const areCategoriesEnabled = (context: AcaRuleContext): boolean => context.appConfig.get('plugins.categoriesEnabled', true); + +export const canDisplayKnowledgeRetrievalButton = (context: AcaRuleContext): boolean => + navigation.isPersonalFiles(context) || + navigation.isSharedFiles(context) || + navigation.isRecentFiles(context) || + navigation.isFavorites(context) || + ((navigation.isSearchResults(context) || navigation.isLibraryContent(context)) && navigation.isNotLibraries(context)); diff --git a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts index fe17f45c2..65aeb52c7 100644 --- a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts +++ b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts @@ -22,7 +22,7 @@ * from Hyland Software. If not, see . */ -import { DocumentListComponent, ShareDataRow, UploadService } from '@alfresco/adf-content-services'; +import { DocumentListComponent, SearchAiInputState, SearchAiService, ShareDataRow, UploadService } from '@alfresco/adf-content-services'; import { ShowHeaderMode } from '@alfresco/adf-core'; import { ContentActionRef, DocumentListPresetRef, SelectionState } from '@alfresco/adf-extensions'; import { OnDestroy, OnInit, OnChanges, ViewChild, SimpleChanges, Directive, inject, HostListener } from '@angular/core'; @@ -77,10 +77,18 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { protected breakpointObserver = inject(BreakpointObserver); protected uploadService = inject(UploadService); protected router = inject(Router); - private fileAutoDownloadService = inject(AcaFileAutoDownloadService, { optional: true }); - + protected searchAiService: SearchAiService = inject(SearchAiService); protected subscriptions: Subscription[] = []; + private fileAutoDownloadService = inject(AcaFileAutoDownloadService, { optional: true }); + private _searchAiInputState: SearchAiInputState = { + active: false + }; + + get searchAiInputState(): SearchAiInputState { + return this._searchAiInputState; + } + ngOnInit() { this.extensions .getCreateActions() @@ -127,6 +135,10 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { .subscribe((result) => { this.isSmallScreen = result.matches; }); + + this.searchAiService.toggleSearchAiInput$ + .pipe(takeUntil(this.onDestroy$)) + .subscribe((searchAiInputState) => (this._searchAiInputState = searchAiInputState)); } ngOnChanges(changes: SimpleChanges) { diff --git a/projects/aca-shared/store/src/actions/search-ai.actions.ts b/projects/aca-shared/store/src/actions/search-ai.actions.ts new file mode 100644 index 000000000..e4d54683e --- /dev/null +++ b/projects/aca-shared/store/src/actions/search-ai.actions.ts @@ -0,0 +1,42 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { Action } from '@ngrx/store'; +import { AiSearchByTermPayload } from '../models/ai-search-by-term-payload'; + +export enum SearchAiActionTypes { + SearchByTermAi = 'SEARCH_BY_TERM_AI', + ToggleAiSearchInput = 'TOGGLE_AI_SEARCH_INPUT' +} + +export class SearchByTermAiAction implements Action { + readonly type = SearchAiActionTypes.SearchByTermAi; + constructor(public payload: AiSearchByTermPayload) {} +} + +export class ToggleAISearchInput implements Action { + readonly type = SearchAiActionTypes.ToggleAiSearchInput; + + constructor(public agentId: string) {} +} diff --git a/projects/aca-shared/store/src/models/ai-search-by-term-payload.ts b/projects/aca-shared/store/src/models/ai-search-by-term-payload.ts new file mode 100644 index 000000000..696d555a3 --- /dev/null +++ b/projects/aca-shared/store/src/models/ai-search-by-term-payload.ts @@ -0,0 +1,28 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +export interface AiSearchByTermPayload { + searchTerm: string; + agentId: string; +} diff --git a/projects/aca-shared/store/src/public-api.ts b/projects/aca-shared/store/src/public-api.ts index efc2a518a..e9437c802 100644 --- a/projects/aca-shared/store/src/public-api.ts +++ b/projects/aca-shared/store/src/public-api.ts @@ -37,11 +37,13 @@ export * from './actions/viewer.actions'; export * from './actions/metadata-aspect.actions'; export * from './actions/template.actions'; export * from './actions/contextmenu.actions'; +export * from './actions/search-ai.actions'; export * from './effects/dialog.effects'; export * from './effects/router.effects'; export * from './effects/snackbar.effects'; +export * from './models/ai-search-by-term-payload'; export * from './models/delete-status.model'; export * from './models/deleted-node-info.model'; export * from './models/node-info.model';