diff --git a/projects/aca-content/assets/app.extensions.json b/projects/aca-content/assets/app.extensions.json index ca545b754..dae13abb2 100644 --- a/projects/aca-content/assets/app.extensions.json +++ b/projects/aca-content/assets/app.extensions.json @@ -128,6 +128,16 @@ } } ], + "icons": [ + { + "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": [ { "id": "app.create.folder", @@ -676,6 +686,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 32d109265..4e5c99d42 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -609,5 +609,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 4182327ba..aa3a121ef 100644 --- a/projects/aca-content/src/lib/aca-content.module.ts +++ b/projects/aca-content/src/lib/aca-content.module.ts @@ -77,6 +77,7 @@ import { ContextMenuComponent } from './components/context-menu/context-menu.com import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component'; +import { AgentsButtonComponent } from './components/knowledge-retrieval/search-ai/agents-button/agents-button.component'; @NgModule({ imports: [ @@ -138,6 +139,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.bulk-actions-dropdown': BulkActionsDropdownComponent, 'app.shared-link.toggleSharedLink': ToggleSharedComponent, @@ -197,6 +199,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 9b90dec77..75190109e 100644 --- a/projects/aca-content/src/lib/components/favorites/favorites.component.html +++ b/projects/aca-content/src/lib/components/favorites/favorites.component.html @@ -1,9 +1,17 @@
-

+ + + +
+

{{ (selectedRowItemsCount < 1 ? 'APP.BROWSE.FAVORITES.TITLE' : 'APP.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}

- + +
+

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 16d5495ee..c17a0a771 100644 --- a/projects/aca-content/src/lib/components/favorites/favorites.component.ts +++ b/projects/aca-content/src/lib/components/favorites/favorites.component.ts @@ -46,6 +46,7 @@ import { import { DocumentListDirective } from '../../directives/document-list.directive'; import { TranslateModule } from '@ngx-translate/core'; import { DocumentListComponent } from '@alfresco/adf-content-services'; +import { SearchAiInputContainerComponent } from '../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -59,6 +60,7 @@ import { DocumentListComponent } from '@alfresco/adf-content-services'; PageLayoutComponent, TranslateModule, ToolbarComponent, + SearchAiInputContainerComponent, EmptyContentComponent, DynamicColumnComponent, DocumentListComponent, @@ -67,7 +69,8 @@ import { DocumentListComponent } from '@alfresco/adf-content-services'; CustomEmptyContentTemplateDirective ], 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 6a63c5d96..35a5d0494 100644 --- a/projects/aca-content/src/lib/components/files/files.component.html +++ b/projects/aca-content/src/lib/components/files/files.component.html @@ -1,12 +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 549f9e471..62746a938 100644 --- a/projects/aca-content/src/lib/components/files/files.component.ts +++ b/projects/aca-content/src/lib/components/files/files.component.ts @@ -58,6 +58,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, @@ -73,6 +74,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; PaginationDirective, PageLayoutComponent, ToolbarComponent, + SearchAiInputContainerComponent, DynamicColumnComponent, BreadcrumbComponent, UploadDragAreaComponent, @@ -82,7 +84,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; CustomEmptyContentTemplateDirective ], 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 80cfa618b..2a2a84fb8 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,9 +1,17 @@
-

+ + + +
+

{{ (selectedRowItemsCount < 1 ? 'APP.BROWSE.RECENT.TITLE' : 'APP.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}

- + +
+

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 9274f94fd..d2b32cba2 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,9 +1,15 @@ - +
- - -
- + + +
+ + +
+ +
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 4b42cbfc1..112adcd66 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%; } 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 2ddf74b44..0598ca69a 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 @@ -77,6 +77,7 @@ import { MatIconModule } from '@angular/material/icon'; import { SearchResultsRowComponent } from '../search-results-row/search-results-row.component'; import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions'; import { BulkActionsDropdownComponent } from '../../bulk-actions-dropdown/bulk-actions-dropdown.component'; +import { SearchAiInputContainerComponent } from '../../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component'; @Component({ standalone: true, @@ -110,7 +111,8 @@ import { BulkActionsDropdownComponent } from '../../bulk-actions-dropdown/bulk-a DateColumnHeaderComponent, CustomEmptyContentTemplateDirective, ViewerToolbarComponent, - BulkActionsDropdownComponent + BulkActionsDropdownComponent, + 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 e8f446754..33e6fe8b0 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,10 +1,18 @@
-

+ + + +
+

{{ (selectedRowItemsCount < 1 ? 'APP.BROWSE.SHARED.TITLE' : 'APP.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}

- + +
+

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 02297d68b..dfeb5d107 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,6 +70,8 @@ import { INITIAL_STATE } from './initial-state'; FavoriteEffects, TemplateEffects, ContextMenuEffects, + SearchAiEffects, + ContextMenuEffects, SnackbarEffects, RouterEffects ]) 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 aa6ec1d4c..882a91f1a 100644 --- a/projects/aca-content/src/lib/ui/variables/variables.scss +++ b/projects/aca-content/src/lib/ui/variables/variables.scss @@ -46,6 +46,8 @@ $disabled-chip-background-color: #f5f5f5; $contrast-gray: mat.get-color-from-palette($foreground, 'secondary-tex'); $search-highlight-background-color: #ffd180; $info-snackbar-background: #1f74db; +$text-light-color: rgba(33, 35, 40, 0.7); +$card-background-grey-color: rgb(248, 248, 248); // CSS Variables $defaults: ( @@ -96,7 +98,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 d97b5f105..06a7dd369 100644 --- a/projects/aca-shared/rules/src/app.rules.ts +++ b/projects/aca-shared/rules/src/app.rules.ts @@ -629,3 +629,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 f404b68f3..c1f1828f6 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, DocumentListService, ShareDataRow, UploadService } from '@alfresco/adf-content-services'; +import { DocumentListComponent, DocumentListService, 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'; @@ -81,8 +81,18 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { protected router = inject(Router); private autoDownloadService = inject(AutoDownloadService, { 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() @@ -135,6 +145,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 e23673eaf..b6ef1adba 100644 --- a/projects/aca-shared/store/src/public-api.ts +++ b/projects/aca-shared/store/src/public-api.ts @@ -37,10 +37,12 @@ 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/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';