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 371982e79..e2b10bc12 100644
--- a/projects/aca-content/src/lib/components/files/files.component.ts
+++ b/projects/aca-content/src/lib/components/files/files.component.ts
@@ -59,7 +59,6 @@ import { CommonModule } from '@angular/common';
import { TranslatePipe } 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';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpErrorResponse } from '@angular/common/http';
import { extractFiltersFromEncodedQuery } from '../../utils/aca-search-utils';
@@ -77,7 +76,6 @@ import { extractFiltersFromEncodedQuery } from '../../utils/aca-search-utils';
PaginationDirective,
PageLayoutComponent,
ToolbarComponent,
- SearchAiInputContainerComponent,
DynamicColumnComponent,
BreadcrumbComponent,
UploadDragAreaComponent,
diff --git a/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.html b/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.html
new file mode 100644
index 000000000..bed30dcae
--- /dev/null
+++ b/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.html
@@ -0,0 +1 @@
+
diff --git a/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.spec.ts b/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.spec.ts
new file mode 100644
index 000000000..e9ab65c6e
--- /dev/null
+++ b/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.spec.ts
@@ -0,0 +1,66 @@
+/*!
+ * Copyright © 2005-2025 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { KnowledgeDiscoverySidenavComponent } from './knowledge-discovery-sidenav.component';
+import { AppTestingModule } from '../../testing/app-testing.module';
+import { AppSettingsService } from '@alfresco/aca-shared';
+
+describe('KnowledgeDiscoverySidenavComponent', () => {
+ let fixture: ComponentFixture
;
+ let component: KnowledgeDiscoverySidenavComponent;
+ let appSettings: AppSettingsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [AppTestingModule, KnowledgeDiscoverySidenavComponent]
+ });
+
+ appSettings = TestBed.inject(AppSettingsService);
+ fixture = TestBed.createComponent(KnowledgeDiscoverySidenavComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should set item with children using URL from config', () => {
+ spyOnProperty(appSettings, 'knowledgeDiscoveryUrl', 'get').and.returnValue('https://discovery.example.com');
+
+ fixture.detectChanges();
+
+ expect(component.item).toEqual({
+ id: 'app.knowledgeDiscovery.sidenav',
+ icon: '',
+ title: 'KNOWLEDGE_RETRIEVAL.SIDENAV.TITLE',
+ route: '/',
+ children: [
+ {
+ id: 'app.knowledgeDiscovery.sidenav.discovery',
+ icon: '',
+ title: 'KNOWLEDGE_RETRIEVAL.SIDENAV.DISCOVERY',
+ route: 'https://discovery.example.com',
+ url: 'https://discovery.example.com'
+ }
+ ]
+ });
+ });
+});
diff --git a/projects/aca-content/src/lib/services/search-ai-navigation.service.ts b/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.ts
similarity index 51%
rename from projects/aca-content/src/lib/services/search-ai-navigation.service.ts
rename to projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.ts
index a4b2850b8..c7e17a38d 100644
--- a/projects/aca-content/src/lib/services/search-ai-navigation.service.ts
+++ b/projects/aca-content/src/lib/components/knowledge-discovery/knowledge-discovery-sidenav.component.ts
@@ -22,33 +22,38 @@
* from Hyland Software. If not, see .
*/
-import { Injectable, inject } from '@angular/core';
-import { Params, Router } from '@angular/router';
-import { SearchAiService } from '@alfresco/adf-content-services';
+import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
+import { NavBarLinkRef } from '@alfresco/adf-extensions';
+import { ExpandMenuComponent } from '../sidenav/components/expand-menu.component';
+import { AppSettingsService } from '@alfresco/aca-shared';
-@Injectable({ providedIn: 'root' })
-export class SearchAiNavigationService {
- private readonly router = inject(Router);
- private readonly searchAiService = inject(SearchAiService);
+@Component({
+ selector: 'aca-knowledge-discovery-sidenav',
+ imports: [ExpandMenuComponent],
+ templateUrl: './knowledge-discovery-sidenav.component.html',
+ encapsulation: ViewEncapsulation.None
+})
+export class KnowledgeDiscoverySidenavComponent implements OnInit {
+ private readonly appSettings = inject(AppSettingsService);
- private readonly knowledgeRetrievalRoute = '/knowledge-retrieval';
+ item: NavBarLinkRef;
- private previousRoute = '';
-
- navigateToPreviousRouteOrCloseInput(): void {
- if (this.router.url.includes(this.knowledgeRetrievalRoute)) {
- void this.router.navigateByUrl(this.previousRoute || '/personal-files');
- } else {
- this.searchAiService.updateSearchAiInputState({
- active: false
- });
- }
- }
-
- navigateToSearchAi(queryParams: Params): void {
- if (!this.router.url.includes(this.knowledgeRetrievalRoute)) {
- this.previousRoute = this.router.url;
- }
- void this.router.navigate([this.knowledgeRetrievalRoute], { queryParams });
+ ngOnInit(): void {
+ const url = this.appSettings.knowledgeDiscoveryUrl;
+ this.item = {
+ id: 'app.knowledgeDiscovery.sidenav',
+ icon: '',
+ title: 'KNOWLEDGE_RETRIEVAL.SIDENAV.TITLE',
+ route: '/',
+ children: [
+ {
+ id: 'app.knowledgeDiscovery.sidenav.discovery',
+ icon: '',
+ title: 'KNOWLEDGE_RETRIEVAL.SIDENAV.DISCOVERY',
+ route: url,
+ url
+ }
+ ]
+ };
}
}
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
deleted file mode 100644
index 182bcaff9..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.html
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
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
deleted file mode 100644
index f3aa6cc7a..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-aca-agents-button.aca-agents-button {
- height: 32px;
- display: block;
-
- button {
- &.aca-agents-menu-button {
- display: flex;
- align-items: end;
-
- &.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;
- padding-bottom: 0;
-
- &-agent {
- height: 40px;
-
- &:not(:last-child) {
- margin-bottom: 2px;
- }
-
- &-content {
- display: flex;
- align-items: center;
-
- &-name {
- width: 120px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-
- adf-avatar {
- margin-right: 12px;
- margin-bottom: 2px;
- padding-left: 1px;
- padding-top: 1px;
-
- .adf-avatar__image {
- cursor: pointer;
- }
- }
- }
- }
-}
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.spec.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.spec.ts
deleted file mode 100644
index be90318ae..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.spec.ts
+++ /dev/null
@@ -1,470 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { ComponentFixture, TestBed } from '@angular/core/testing';
-import { AgentsButtonComponent } from './agents-button.component';
-import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
-import { Subject } from 'rxjs';
-import { By } from '@angular/platform-browser';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
-import { getAppSelection, SearchAiActionTypes } from '@alfresco/aca-shared/store';
-import { AvatarComponent, NoopTranslateModule, NotificationService } from '@alfresco/adf-core';
-import { SelectionState } from '@alfresco/adf-extensions';
-import { MatMenu, MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
-import { HarnessLoader } from '@angular/cdk/testing';
-import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
-import { MatSelectionListHarness } from '@angular/material/list/testing';
-import { MatMenuHarness } from '@angular/material/menu/testing';
-import { MatSelectionList } from '@angular/material/list';
-import { MatSnackBarRef } from '@angular/material/snack-bar';
-import { ChangeDetectorRef } from '@angular/core';
-import { Agent, KnowledgeRetrievalConfigEntry } from '@alfresco/js-api';
-import { MatIconTestingModule } from '@angular/material/icon/testing';
-
-describe('AgentsButtonComponent', () => {
- let component: AgentsButtonComponent;
- let fixture: ComponentFixture;
- let agents$: Subject;
- let agentsMock: Agent[];
- let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>;
- let selectionState: SelectionState;
- let store: MockStore;
- let config$: Subject;
-
- const knowledgeRetrievalUrl = 'some url';
-
- const getMenu = (): MatMenu => fixture.debugElement.query(By.directive(MatMenu)).componentInstance;
-
- const getAgentsButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('.aca-agents-menu-button'))?.nativeElement;
-
- const runButtonActions = (eventName: string): void => {
- let event: Event;
- let notificationService: NotificationService;
- let message: string;
-
- beforeEach(() => {
- config$.next({
- entry: {
- knowledgeRetrievalUrl
- }
- });
- config$.complete();
- event =
- eventName === 'mouseup'
- ? new MouseEvent(eventName)
- : new KeyboardEvent(eventName, {
- key: 'Enter'
- });
- agents$.next(agentsMock);
- agents$.complete();
- spyOn(window, 'open');
- notificationService = TestBed.inject(NotificationService);
- spyOn(notificationService, 'showError');
- message = 'Some message';
- });
-
- const getMenuTrigger = (): MatMenuPanel => fixture.debugElement.query(By.directive(MatMenuTrigger)).injector.get(MatMenuTrigger).menu;
-
- const testButtonActions = (): void => {
- it('should not display notification if checkSearchAvailability from SearchAiService returns empty message', () => {
- message = '';
- checkSearchAvailabilitySpy.and.returnValue(message);
-
- getAgentsButton().dispatchEvent(event);
- expect(notificationService.showError).not.toHaveBeenCalled();
- });
-
- it('should disable menu triggering if checkSearchAvailability from SearchAiService returns message', () => {
- checkSearchAvailabilitySpy.and.returnValue('Some message');
-
- getAgentsButton().dispatchEvent(event);
- fixture.detectChanges();
- expect(getMenuTrigger()).toBeNull();
- });
- };
-
- describe('with selected nodes', () => {
- beforeEach(() => {
- selectionState.isEmpty = false;
- });
-
- it('should display notification if checkSearchAvailability from SearchAiService returns message', () => {
- checkSearchAvailabilitySpy.and.returnValue(message);
-
- getAgentsButton().dispatchEvent(event);
- expect(notificationService.showError).toHaveBeenCalledWith(message);
- });
-
- testButtonActions();
-
- it('should enable menu triggering if checkSearchAvailability from SearchAiService returns empty message', () => {
- checkSearchAvailabilitySpy.and.returnValue('');
-
- getAgentsButton().dispatchEvent(event);
- fixture.detectChanges();
- const menuTrigger = getMenuTrigger();
- expect(menuTrigger).toBeTruthy();
- expect(menuTrigger).toBe(getMenu());
- });
-
- it('should call checkSearchAvailability from SearchAiService with correct parameter', () => {
- getAgentsButton().dispatchEvent(event);
-
- expect(checkSearchAvailabilitySpy).toHaveBeenCalledWith(selectionState);
- });
-
- it('should not open new tab for url loaded from config', () => {
- getAgentsButton().dispatchEvent(event);
-
- expect(window.open).not.toHaveBeenCalled();
- });
- });
-
- describe('without selected nodes', () => {
- it('should not display notification if checkSearchAvailability from SearchAiService returns message', () => {
- checkSearchAvailabilitySpy.and.returnValue(message);
-
- getAgentsButton().dispatchEvent(event);
- expect(notificationService.showError).not.toHaveBeenCalled();
- });
-
- testButtonActions();
-
- it('should disable menu triggering if checkSearchAvailability from SearchAiService returns empty message', () => {
- checkSearchAvailabilitySpy.and.returnValue('');
-
- getAgentsButton().dispatchEvent(event);
- fixture.detectChanges();
- expect(getMenuTrigger()).toBeNull();
- });
-
- it('should not call checkSearchAvailability from SearchAiService', () => {
- getAgentsButton().dispatchEvent(event);
-
- expect(checkSearchAvailabilitySpy).not.toHaveBeenCalled();
- });
-
- it('should open new tab for url loaded from config', () => {
- getAgentsButton().dispatchEvent(event);
-
- expect(window.open).toHaveBeenCalledWith(knowledgeRetrievalUrl);
- });
- });
- };
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [NoopTranslateModule, MatIconTestingModule, AgentsButtonComponent],
- providers: [provideMockStore({})]
- });
-
- fixture = TestBed.createComponent(AgentsButtonComponent);
- component = fixture.componentInstance;
- store = TestBed.inject(MockStore);
- agents$ = new Subject();
- spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
- agentsMock = [
- {
- id: '1',
- name: 'HR Agent',
- description: 'Test 1',
- avatarUrl: undefined
- },
- {
- id: '2',
- name: 'Policy Agent',
- description: 'Test 2',
- avatarUrl: undefined
- }
- ];
- const searchAiService = TestBed.inject(SearchAiService);
- checkSearchAvailabilitySpy = spyOn(searchAiService, 'checkSearchAvailability');
- config$ = new Subject();
- spyOn(searchAiService, 'getConfig').and.returnValue(config$);
- selectionState = {
- nodes: [],
- isEmpty: true,
- count: 0,
- libraries: []
- };
- store.overrideSelector(getAppSelection, selectionState);
- fixture.detectChanges();
- });
-
- afterEach(() => {
- store.resetSelectors();
- });
-
- describe('Button', () => {
- let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef>;
-
- beforeEach(() => {
- const notificationService = TestBed.inject(NotificationService);
- notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
- });
-
- describe('loaded config', () => {
- beforeEach(() => {
- config$.next({
- entry: {
- knowledgeRetrievalUrl
- }
- });
- config$.complete();
- });
-
- it('should be rendered if any agentsMock are loaded', () => {
- agents$.next(agentsMock);
- agents$.complete();
- fixture.detectChanges();
-
- expect(getAgentsButton()).toBeTruthy();
- });
-
- it('should get agentsMock on component init', () => {
- agents$.next(agentsMock);
- agents$.complete();
- component.ngOnInit();
-
- expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' });
- expect(component.agents).toEqual(agentsMock);
- expect(notificationServiceSpy).not.toHaveBeenCalled();
- });
-
- it('should run detectChanges when getting the agentsMock', () => {
- const changeDetectorRef2 = fixture.debugElement.injector.get(ChangeDetectorRef);
- const detectChangesSpy = spyOn(changeDetectorRef2.constructor.prototype, 'detectChanges');
-
- component.ngOnInit();
- agents$.next(agentsMock);
-
- expect(detectChangesSpy).toHaveBeenCalled();
- });
-
- it('should show notification error on getAgents error', () => {
- agents$.error('error');
- component.ngOnInit();
-
- expect(component.agents).toEqual([]);
- expect(component.initialsByAgentId).toEqual({});
- expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING');
- });
-
- it('should not be rendered if none agent is loaded', () => {
- agentsMock = [];
- agents$.next(agentsMock);
- agents$.complete();
-
- fixture.detectChanges();
- expect(getAgentsButton()).toBeFalsy();
- });
-
- it('should have correct label', () => {
- agents$.next(agentsMock);
- agents$.complete();
- fixture.detectChanges();
-
- expect(getAgentsButton().textContent.trim()).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.AGENTS_BUTTON.LABEL');
- });
-
- it('should contain stars icon', () => {
- agents$.next(agentsMock);
- agents$.complete();
- fixture.detectChanges();
-
- expect(fixture.debugElement.query(By.css('.aca-agents-menu-button adf-icon')).componentInstance.value).toBe('adf:colored-stars-ai');
- });
- });
-
- describe('loaded config with error', () => {
- beforeEach(() => {
- config$.error('error');
- config$.complete();
- });
-
- it('should not be rendered', () => {
- agents$.next(agentsMock);
- agents$.complete();
- fixture.detectChanges();
-
- expect(getAgentsButton()).toBeFalsy();
- });
-
- it('should show notification error', () => {
- agents$.next(agentsMock);
- agents$.complete();
- component.ngOnInit();
-
- expect(component.hxInsightUrl).toBeUndefined();
- expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.HX_INSIGHT_URL_FETCHING');
- });
- });
- });
-
- const buttonKeyboardActions = (eventName: string): void => {
- describe(`Button action - ${eventName} event`, () => {
- runButtonActions(eventName);
- });
- };
-
- ['mouseup', 'keydown'].forEach((eventName) => {
- buttonKeyboardActions(eventName);
- });
-
- describe('Agents menu', () => {
- let loader: HarnessLoader;
-
- const prepareData = (agents: Agent[]): void => {
- config$.next({
- entry: {
- knowledgeRetrievalUrl
- }
- });
- config$.complete();
- loader = TestbedHarnessEnvironment.loader(fixture);
- agents$.next(agents);
- selectionState.isEmpty = false;
- checkSearchAvailabilitySpy.and.returnValue('');
- const button = getAgentsButton();
- button.dispatchEvent(new MouseEvent('mouseup'));
- fixture.detectChanges();
- button.click();
- fixture.detectChanges();
- };
-
- const getAvatar = (agentId: string): AvatarComponent =>
- fixture.debugElement.query(By.css(`[data-automation-id=aca-agents-button-agent-${agentId}]`)).query(By.directive(AvatarComponent))
- .componentInstance;
-
- describe('Agents position', () => {
- it('should have assigned before to xPosition', () => {
- prepareData(agentsMock);
- agents$.complete();
- expect(getMenu().xPosition).toBe('before');
- });
- });
-
- describe('Agents multi words name', () => {
- beforeEach(() => {
- prepareData(agentsMock);
- agents$.complete();
- });
-
- const getAgentsListHarness = async (): Promise =>
- (await loader.getHarness(MatMenuHarness)).getHarness(MatSelectionListHarness);
-
- const selectAgent = async (): Promise =>
- (await getAgentsListHarness()).selectItems({
- fullText: 'PA Policy Agent'
- });
-
- const getAgentsList = (): MatSelectionList => fixture.debugElement.query(By.directive(MatSelectionList)).componentInstance;
-
- it('should deselect selected agent after selecting other', async () => {
- component.data = {
- trigger: SearchAiActionTypes.ToggleAiSearchInput
- };
- const selectionList = getAgentsList();
- spyOn(selectionList, 'deselectAll');
- await selectAgent();
-
- expect(selectionList.deselectAll).toHaveBeenCalled();
- });
-
- it('should dispatch on store selected agent', async () => {
- component.data = {
- trigger: SearchAiActionTypes.ToggleAiSearchInput
- };
- spyOn(store, 'dispatch');
- await selectAgent();
-
- expect(store.dispatch).toHaveBeenCalledWith(
- jasmine.objectContaining({
- type: SearchAiActionTypes.ToggleAiSearchInput,
- agentId: '2'
- })
- );
- });
-
- it('should disallow selecting multiple agentsMock', () => {
- expect(getAgentsList().multiple).toBeFalse();
- });
-
- it('should have hidden single selection indicator', () => {
- expect(getAgentsList().hideSingleSelectionIndicator).toBeTrue();
- });
-
- it('should display option for each agent', async () => {
- const agents = await (await getAgentsListHarness()).getItems();
- expect(agents.length).toBe(2);
- expect(await agents[0].getFullText()).toBe('HA HR Agent');
- expect(await agents[1].getFullText()).toBe('PA Policy Agent');
- });
-
- it('should display avatar for each agent', () => {
- expect(getAvatar('1')).toBeTruthy();
- expect(getAvatar('2')).toBeTruthy();
- });
-
- it('should assign correct initials to each avatar for each agent with double section name', () => {
- expect(getAvatar('1').initials).toBe('HA');
- expect(getAvatar('2').initials).toBe('PA');
- });
-
- it('should assign correct src to each avatar', () => {
- agentsMock[0].avatarUrl = 'some-url-1';
- agentsMock[1].avatarUrl = 'some-url-2';
-
- fixture.detectChanges();
- expect(getAvatar('1').src).toBe('some-url-1');
- expect(getAvatar('2').src).toBe('some-url-2');
- });
- });
-
- describe('Agents single word name', () => {
- it('should assign correct initials to each avatar for each agent with single section name', () => {
- agentsMock = [
- {
- id: '1',
- name: 'HR Agent',
- description: 'Test 1',
- avatarUrl: undefined
- },
- {
- id: '2',
- name: 'Policy Agent',
- description: 'Test 2',
- avatarUrl: undefined
- }
- ];
- agentsMock[0].name = 'Adam';
- agentsMock[1].name = 'Bob';
- prepareData(agentsMock);
-
- expect(getAvatar('1').initials).toBe('A');
- expect(getAvatar('2').initials).toBe('B');
- });
- });
- });
-});
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
deleted file mode 100644
index bfffd1027..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/agents-button/agents-button.component.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { ChangeDetectorRef, Component, DestroyRef, inject, Input, 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 { forkJoin, throwError } from 'rxjs';
-import { catchError, take } from 'rxjs/operators';
-import { MatMenuModule } from '@angular/material/menu';
-import { MatListModule, MatSelectionListChange } from '@angular/material/list';
-import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { Agent } from '@alfresco/js-api';
-import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-
-@Component({
- imports: [CommonModule, MatMenuModule, MatListModule, TranslatePipe, AvatarComponent, IconComponent, MatTooltipModule],
- 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 {
- private readonly store = inject>(Store);
- private readonly notificationService = inject(NotificationService);
- private readonly searchAiService = inject(SearchAiService);
- private readonly agentService = inject(AgentService);
- private readonly translateService = inject(TranslateService);
- private readonly cd = inject(ChangeDetectorRef);
-
- @Input()
- data: { trigger: string };
-
- private selectedNodesState: SelectionState;
- private _agents: Agent[] = [];
- private _disabled = true;
- private _initialsByAgentId: { [key: string]: string } = {};
- private _hxInsightUrl: string;
-
- get agents(): Agent[] {
- return this._agents;
- }
-
- get disabled(): boolean {
- return this._disabled;
- }
-
- get initialsByAgentId(): { [key: string]: string } {
- return this._initialsByAgentId;
- }
-
- get hxInsightUrl(): string {
- return this._hxInsightUrl;
- }
-
- private readonly destroyRef = inject(DestroyRef);
-
- ngOnInit(): void {
- this.store
- .select(getAppSelection)
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((selection) => {
- this.selectedNodesState = selection;
- });
- forkJoin({
- agents: this.agentService.getAgents().pipe(
- take(1),
- catchError(() => throwError('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING'))
- ),
- config: this.searchAiService.getConfig().pipe(catchError(() => throwError('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.HX_INSIGHT_URL_FETCHING')))
- }).subscribe(
- (result) => {
- this._hxInsightUrl = result.config.entry.knowledgeRetrievalUrl;
- this._agents = result.agents;
-
- this.cd.detectChanges();
-
- 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;
- }, {});
- }
- },
- (error: string) => this.notificationService.showError(this.translateService.instant(error))
- );
- }
-
- onClick(): void {
- if (!this.selectedNodesState.isEmpty) {
- const message = this.searchAiService.checkSearchAvailability(this.selectedNodesState);
- if (message) {
- this.notificationService.showError(message);
- }
- this._disabled = !!message;
- return;
- }
- this._disabled = true;
- open(this.hxInsightUrl);
- }
-
- 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
deleted file mode 100644
index cc101cd54..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
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
deleted file mode 100644
index 6fd639575..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-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);
- }
-
- .aca-search-ai-input-container-close {
- display: flex;
- }
-}
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.spec.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.spec.ts
deleted file mode 100644
index 701397c8d..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.spec.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { SearchAiInputContainerComponent } from './search-ai-input-container.component';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { SearchAiInputComponent } from '../search-ai-input/search-ai-input.component';
-import { By } from '@angular/platform-browser';
-import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
-import { of, Subject } from 'rxjs';
-import { MatDivider } from '@angular/material/divider';
-import { DebugElement } from '@angular/core';
-import { MatIconButton } from '@angular/material/button';
-import { MatIcon } from '@angular/material/icon';
-import { SearchAiNavigationService } from '../../../../services/search-ai-navigation.service';
-import { NavigationEnd, provideRouter, Router, RouterEvent } from '@angular/router';
-import { getAppSelection } from '@alfresco/aca-shared/store';
-import { NoopTranslateModule } from '@alfresco/adf-core';
-import { MatIconTestingModule } from '@angular/material/icon/testing';
-
-describe('SearchAiInputContainerComponent', () => {
- const routingEvents$: Subject = new Subject();
-
- let component: SearchAiInputContainerComponent;
- let fixture: ComponentFixture;
- let searchAiService: SearchAiService;
- let store: MockStore;
- let mockSearchAiService: jasmine.SpyObj;
- let searchNavigationService: SearchAiNavigationService;
- let mockRouter: any;
-
- beforeEach(() => {
- mockSearchAiService = jasmine.createSpyObj('SearchAiService', ['updateSearchAiInputState'], {
- toggleSearchAiInput$: of(true)
- });
-
- mockRouter = {
- url: '/some-url',
- events: routingEvents$.asObservable(),
- routerState: {
- root: {}
- },
- snapshot: {}
- };
-
- TestBed.configureTestingModule({
- imports: [NoopTranslateModule, MatIconTestingModule, SearchAiInputContainerComponent],
- providers: [
- provideRouter([]),
- { provide: Router, useValue: mockRouter },
- provideMockStore(),
- { provide: SearchAiService, useValue: mockSearchAiService },
- {
- provide: AgentService,
- useValue: {
- getAgents: () =>
- of([
- {
- id: '1',
- name: 'HR Agent',
- description: 'HR Agent',
- avatar: 'avatar1'
- }
- ])
- }
- }
- ]
- });
-
- fixture = TestBed.createComponent(SearchAiInputContainerComponent);
- component = fixture.componentInstance;
- store = TestBed.inject(MockStore);
- searchAiService = TestBed.inject(SearchAiService);
- searchNavigationService = TestBed.inject(SearchAiNavigationService);
- store.overrideSelector(getAppSelection, {
- nodes: [],
- isEmpty: true,
- count: 0,
- libraries: []
- });
- component.agentId = '1';
- fixture.detectChanges();
- });
-
- afterEach(() => {
- store.resetSelectors();
- });
-
- describe('Search ai input', () => {
- let inputComponent: SearchAiInputComponent;
-
- beforeEach(() => {
- inputComponent = fixture.debugElement.query(By.directive(SearchAiInputComponent)).componentInstance;
- });
-
- it('should have assigned correct default placeholder', () => {
- expect(inputComponent.placeholder).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.DEFAULT_PLACEHOLDER');
- });
-
- it('should have assigned correct placeholder if placeholder has been changed', () => {
- component.placeholder = 'Some placeholder';
- fixture.detectChanges();
-
- expect(inputComponent.placeholder).toBe(component.placeholder);
- });
-
- it('should have assigned correct agentId', () => {
- expect(inputComponent.agentId).toBe(component.agentId);
- });
-
- it('should have assigned correct usedInAiResultsPage flag', () => {
- component.usedInAiResultsPage = true;
- fixture.detectChanges();
-
- expect(inputComponent.usedInAiResultsPage).toBeTrue();
- });
-
- it('should set inputState$ to toggleSearchAiInput$ from the service on ngOnInit', () => {
- component.ngOnInit();
-
- expect(component.inputState$).toBe(mockSearchAiService.toggleSearchAiInput$);
- });
- });
-
- describe('Divider', () => {
- it('should have a vertical divider', () => {
- fixture.detectChanges();
-
- expect(fixture.debugElement.query(By.directive(MatDivider)).componentInstance.vertical).toBeTrue();
- });
- });
-
- describe('Leaving button', () => {
- let button: DebugElement;
-
- beforeEach(() => {
- button = fixture.debugElement.query(By.directive(MatIconButton));
- });
-
- it('should have correct title when page is not knowledge-retrieval', () => {
- mockRouter.url = '/other-page';
-
- component.ngOnInit();
-
- expect(button.nativeElement.title).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.HIDE_INPUT');
- });
-
- it('should have correct title when page is knowledge-retrieval', () => {
- mockRouter.url = '/knowledge-retrieval/some-data';
-
- component.ngOnInit();
- fixture.detectChanges();
-
- expect(button.nativeElement.title).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.HIDE_ANSWER');
- });
-
- it('should contain close icon', () => {
- expect(button.query(By.directive(MatIcon)).nativeElement.textContent).toBe('close');
- });
-
- it('should call navigateToPreviousRoute on SearchAiService when clicked', () => {
- spyOn(searchNavigationService, 'navigateToPreviousRouteOrCloseInput');
- button.nativeElement.click();
-
- expect(searchNavigationService.navigateToPreviousRouteOrCloseInput).toHaveBeenCalled();
- });
-
- it('should not call updateSearchAiInputState on SearchAiService when there is different event than navigation starts', () => {
- routingEvents$.next(new NavigationEnd(1, '', ''));
-
- expect(searchAiService.updateSearchAiInputState).not.toHaveBeenCalled();
- });
- });
-});
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
deleted file mode 100644
index 29229befc..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*!
- * Copyright © 2005-2025 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, OnInit, ViewEncapsulation, inject } 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 { SearchAiInputState, SearchAiService } from '@alfresco/adf-content-services';
-import { TranslatePipe } from '@ngx-translate/core';
-import { Observable } from 'rxjs';
-import { AsyncPipe } from '@angular/common';
-import { Router } from '@angular/router';
-
-@Component({
- imports: [SearchAiInputComponent, MatIconModule, MatDividerModule, MatButtonModule, TranslatePipe, AsyncPipe],
- 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 {
- private readonly searchAiService = inject(SearchAiService);
- private readonly searchNavigationService = inject(SearchAiNavigationService);
- private readonly router = inject(Router);
-
- @Input()
- placeholder = 'KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.DEFAULT_PLACEHOLDER';
- @Input()
- agentId: string;
- @Input()
- usedInAiResultsPage: boolean;
-
- inputState$: Observable;
- isKnowledgeRetrievalPage = false;
-
- ngOnInit(): void {
- this.isKnowledgeRetrievalPage = this.router.url.startsWith('/knowledge-retrieval');
- this.inputState$ = this.searchAiService.toggleSearchAiInput$;
- }
-
- leaveSearchInput(): void {
- this.searchNavigationService.navigateToPreviousRouteOrCloseInput();
- }
-}
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
deleted file mode 100644
index 6c5c08999..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.html
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
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
deleted file mode 100644
index bf9d471c9..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.scss
+++ /dev/null
@@ -1,191 +0,0 @@
-@use '../../../../ui/mat-selectors' as ms;
-@use '@angular/material' as mat;
-
-aca-search-ai-input {
- width: 100%;
- display: flex;
- align-items: center;
-
- .aca-search-ai-input-text {
- margin-top: 4px;
- 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: 0;
- padding-right: 12px;
- height: 32px;
- border-radius: 6px;
- width: 92px;
- font-weight: 600;
-
- @include mat.button-overrides(
- (
- filled-container-color: var(--theme-primary-color),
- filled-label-text-color: var(--theme-primary-color-default-contrast)
- )
- );
-
- &-label {
- vertical-align: super;
- }
-
- adf-icon {
- margin-bottom: 3px;
- margin-right: 7px;
-
- svg {
- width: 34px;
- height: 34px;
- margin-left: -6px;
- margin-top: -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;
-
- #{ms.$mat-select-trigger} {
- height: auto;
- margin-top: 4px;
- }
-
- &:focus {
- outline: -webkit-focus-ring-color auto 1px;
- }
-
- &-displayed-value {
- display: flex;
- align-items: center;
-
- &-text {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-
- adf-avatar {
- margin-left: 2px;
- margin-right: 6px;
- padding-top: 1px;
- padding-bottom: 3px;
-
- .adf-avatar__image {
- cursor: pointer;
- }
- }
- }
-}
-
-.aca-search-ai-input-agent-select-options.aca-search-ai-input-agent-select-agents#{ms.$mat-select-panel} {
- margin-top: 9px;
-
- .aca-search-ai-input-agent-select-options-option {
- padding-left: 11px;
- padding-right: 11px;
-
- &-content {
- display: flex;
- align-items: center;
- padding-top: 1px;
- padding-bottom: 1px;
-
- &-text {
- width: 120px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-
- adf-avatar {
- margin-right: 12px;
- padding-left: 1px;
-
- .adf-avatar__image {
- cursor: pointer;
- }
- }
- }
-}
-
-.aca-search-ai-input-agent-container {
- position: relative;
-
- .aca-search-ai-input-agent-popup-hover-card {
- display: none;
- position: absolute;
- left: 0;
- z-index: 1;
-
- &-container {
- width: 315px;
- height: fit-content;
- border-radius: 12px;
- margin-top: 4px;
-
- &-title {
- display: flex;
- align-items: center;
- font-size: 20px;
- font-weight: 700;
- padding: 16px 16px 8px;
- gap: 4px;
-
- &-name {
- margin: 0 12px;
- }
-
- img {
- height: 50px;
- width: 50px;
- min-width: 50px;
- min-height: 50px;
- }
-
- span {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-right: 14px;
- }
- }
-
- &-content {
- display: flex;
- color: var(--theme-content-color);
- text-align: justify;
- text-justify: inter-word;
- }
- }
- }
-
- &:hover {
- .aca-search-ai-input-agent-popup-hover-card {
- display: block;
- }
- }
-}
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.spec.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.spec.ts
deleted file mode 100644
index fbe74b3e4..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.spec.ts
+++ /dev/null
@@ -1,489 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { ComponentFixture, TestBed } from '@angular/core/testing';
-import { SearchAiInputComponent } from './search-ai-input.component';
-import { MatSelect, MatSelectModule } from '@angular/material/select';
-import { By } from '@angular/platform-browser';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
-import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
-import { getAppSelection, SearchByTermAiAction, ToggleAISearchInput } from '@alfresco/aca-shared/store';
-import { of, Subject } from 'rxjs';
-import { Agent, NodeEntry } from '@alfresco/js-api';
-import { FormControlDirective } from '@angular/forms';
-import { DebugElement } from '@angular/core';
-import {
- AvatarComponent,
- IconComponent,
- NoopTranslateModule,
- NotificationService,
- UnsavedChangesDialogComponent,
- UserPreferencesService
-} from '@alfresco/adf-core';
-import { HarnessLoader } from '@angular/cdk/testing';
-import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
-import { MatSelectHarness } from '@angular/material/select/testing';
-import { MatOptionHarness } from '@angular/material/core/testing';
-import { MatInput } from '@angular/material/input';
-import { MatButton } from '@angular/material/button';
-import { MatInputHarness } from '@angular/material/input/testing';
-import { SelectionState } from '@alfresco/adf-extensions';
-import { MatSnackBarRef } from '@angular/material/snack-bar';
-import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
-import { ActivatedRoute } from '@angular/router';
-import { ModalAiService } from '../../../../services/modal-ai.service';
-import { MatIconTestingModule } from '@angular/material/icon/testing';
-
-describe('SearchAiInputComponent', () => {
- let component: SearchAiInputComponent;
- let fixture: ComponentFixture;
- let loader: HarnessLoader;
- let selectionState: SelectionState;
- let store: MockStore;
- let agents$: Subject;
- let dialog: MatDialog;
- let activatedRoute: ActivatedRoute;
- let userPreferencesService: UserPreferencesService;
- let agentList: Agent[];
-
- const newSelectionState: SelectionState = {
- ...selectionState,
- file: {
- entry: {
- id: 'some-id'
- }
- } as NodeEntry
- };
-
- const prepareBeforeTest = (): void => {
- selectionState = {
- nodes: [],
- isEmpty: true,
- count: 0,
- libraries: []
- };
- store.overrideSelector(getAppSelection, selectionState);
- component.agentId = '2';
- component.ngOnInit();
- fixture.detectChanges();
- };
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [NoopTranslateModule, MatIconTestingModule, SearchAiInputComponent, MatSelectModule],
- providers: [
- provideMockStore(),
- {
- provide: ActivatedRoute,
- useValue: {
- snapshot: {
- queryParams: { query: 'some query' }
- }
- }
- }
- ]
- });
-
- activatedRoute = TestBed.inject(ActivatedRoute);
- fixture = TestBed.createComponent(SearchAiInputComponent);
- component = fixture.componentInstance;
- store = TestBed.inject(MockStore);
- loader = TestbedHarnessEnvironment.loader(fixture);
- agents$ = new Subject();
- dialog = TestBed.inject(MatDialog);
- userPreferencesService = TestBed.inject(UserPreferencesService);
- spyOn(userPreferencesService, 'get').and.returnValue(JSON.stringify(newSelectionState));
- spyOn(userPreferencesService, 'set');
- spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
- agentList = [
- {
- id: '1',
- name: 'HR Agent',
- description: 'Test 1',
- avatarUrl: undefined
- },
- {
- id: '2',
- name: 'Policy Agent',
- description: 'Test 2',
- avatarUrl: undefined
- }
- ];
- prepareBeforeTest();
- });
-
- afterEach(() => {
- store.resetSelectors();
- });
-
- describe('Agent select box', () => {
- let selectElement: DebugElement;
- let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef>;
-
- beforeEach(() => {
- selectElement = fixture.debugElement.query(By.directive(MatSelect));
- const notificationService = TestBed.inject(NotificationService);
- notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
- });
-
- it('should have assigned formControl', () => {
- expect(selectElement.injector.get(FormControlDirective).form).toBe(component.agentControl);
- });
-
- it('should have hidden single selection indicator', () => {
- expect(selectElement.componentInstance.hideSingleSelectionIndicator).toBeTrue();
- });
-
- it('should set queryControl value to searchTerm if searchTerm is defined', () => {
- const query = 'some new query';
- component.searchTerm = query;
-
- component.ngOnInit();
-
- expect(component.queryControl.value).toBe(query);
- });
-
- it('should set queryControl value to "some new query" if usedInAiResultsPage is equal to false', () => {
- const query = 'some new query';
- component.usedInAiResultsPage = false;
- component.searchTerm = query;
-
- component.ngOnInit();
-
- expect(component.queryControl.value).toBe('some new query');
- });
-
- it('should set queryControl value to empty string if usedInAiResultsPage is equal to true', () => {
- const query = 'some new query';
- component.usedInAiResultsPage = true;
- component.searchTerm = query;
-
- component.ngOnInit();
-
- expect(component.queryControl.value).toBe('');
- });
-
- it('should get agents on init', () => {
- agents$.next(agentList);
- component.ngOnInit();
- expect(component.agents).toEqual(agentList);
- expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' });
- expect(notificationServiceSpy).not.toHaveBeenCalled();
- });
-
- it('should show notification on getAgents error', () => {
- agents$.error('error');
- component.ngOnInit();
-
- expect(component.agents).toEqual([]);
- expect(component.initialsByAgentId).toEqual({});
- expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING');
- });
-
- it('should have selected correct agent', async () => {
- agentList[0].avatarUrl = 'some-url-1';
- agentList[1].avatarUrl = 'some-url-2';
-
- agents$.next(agentList);
- expect(await (await loader.getHarness(MatSelectHarness)).getValueText()).toBe('Policy Agent');
- const avatar = selectElement.query(By.directive(AvatarComponent))?.componentInstance;
- expect(avatar.initials).toBe('PA');
- expect(avatar.size).toBe('26px');
- expect(avatar.src).toBe('some-url-2');
- });
-
- describe('Agents options', () => {
- let options: MatOptionHarness[];
-
- const getAvatarForAgent = (agentId: string): AvatarComponent =>
- fixture.debugElement.query(By.css(`[data-automation-id=aca-search-ai-input-agent-${agentId}]`)).query(By.directive(AvatarComponent))
- .componentInstance;
-
- beforeEach(async () => {
- agents$.next(agentList);
- const selectHarness = await loader.getHarness(MatSelectHarness);
- await selectHarness.open();
- options = await selectHarness.getOptions();
- });
-
- it('should have correct number of agents', () => {
- expect(options.length).toBe(2);
- });
-
- it('should have correct agent names', async () => {
- expect(await options[0].getText()).toBe('HAHR Agent');
- expect(await options[1].getText()).toBe('PAPolicy Agent');
- });
-
- it('should display avatar for each agent', () => {
- expect(getAvatarForAgent('1')).toBeTruthy();
- expect(getAvatarForAgent('2')).toBeTruthy();
- });
-
- it('should have correct initials for avatars for each of agent', () => {
- expect(getAvatarForAgent('1').initials).toBe('HA');
- expect(getAvatarForAgent('2').initials).toBe('PA');
- });
-
- it('should have correct initials for avatars for each of agent', () => {
- agentList[0].avatarUrl = 'some-url-1';
- agentList[1].avatarUrl = 'some-url-2';
-
- fixture.detectChanges();
- expect(getAvatarForAgent('1').src).toBe('some-url-1');
- expect(getAvatarForAgent('2').src).toBe('some-url-2');
- });
-
- it('should assign correct initials to each avatar for each agent with single section name', () => {
- const newAgentList = [
- { ...agentList[0], name: 'Adam' },
- { ...agentList[1], name: 'Bob' }
- ];
- agents$.next(newAgentList);
- fixture.detectChanges();
-
- expect(getAvatarForAgent('1').initials).toBe('A');
- expect(getAvatarForAgent('2').initials).toBe('B');
- });
- });
- });
-
- describe('Agents popup', () => {
- it('should have selected correct agent', () => {
- agentList[0].avatarUrl = 'some-url-1';
- agentList[1].avatarUrl = 'some-url-2';
- agents$.next(agentList);
-
- fixture.detectChanges();
- fixture.debugElement.query(By.css('.aca-search-ai-input-agent-container')).nativeElement.dispatchEvent(new MouseEvent('mouseenter'));
- expect(fixture.debugElement.query(By.css('.aca-search-ai-input-agent-popup-hover-card-container-title adf-avatar')).componentInstance.src).toBe(
- 'some-url-2'
- );
- });
- });
-
- describe('Query input', () => {
- let queryInput: DebugElement;
-
- beforeEach(() => {
- queryInput = fixture.debugElement.query(By.directive(MatInput));
- agents$.next(agentList);
- });
-
- it('should have assigned formControl', () => {
- fixture.detectChanges();
-
- expect(queryInput.injector.get(FormControlDirective).form).toBe(component.queryControl);
- });
-
- it('should have assigned correct placeholder', () => {
- component.placeholder = 'Please ask your question with as much detail as possible...';
-
- expect(queryInput.componentInstance.placeholder).toBe(component.placeholder);
- });
-
- testSubmitting(false);
- });
-
- describe('Submit button', () => {
- let submitButton: DebugElement;
- let queryInput: MatInputHarness;
-
- beforeEach(async () => {
- submitButton = fixture.debugElement.query(By.directive(MatButton));
- queryInput = await loader.getHarness(MatInputHarness);
- agents$.next(agentList);
- });
-
- it('should be disabled by default', () => {
- activatedRoute.snapshot.queryParams = { query: '' };
- component.ngOnInit();
- fixture.detectChanges();
-
- expect(submitButton.nativeElement.disabled).toBeTrue();
- });
-
- it('should be enabled if query input is filled', async () => {
- await queryInput.setValue('Some question');
-
- expect(submitButton.nativeElement.disabled).toBeFalse();
- });
-
- it('should be disabled if query input was filled but after that it was emptied', async () => {
- await queryInput.setValue('Some question');
- await queryInput.setValue('');
-
- expect(submitButton.nativeElement.disabled).toBeTrue();
- });
-
- it('should contain stars icon', () => {
- expect(submitButton.query(By.directive(IconComponent)).componentInstance.value).toBe('adf:three_magic_stars_ai');
- });
-
- it('should have correct label', () => {
- expect(submitButton.nativeElement.textContent.trim()).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.ASK_BUTTON_LABEL');
- });
-
- testSubmitting();
- });
-
- function testSubmitting(useButton = true) {
- describe('Submitting', () => {
- let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>;
- let notificationService: NotificationService;
- let submitButton: DebugElement;
- let queryInput: MatInputHarness;
- let submittingTrigger: () => void;
- const query = 'some query';
- let dialogOpenSpy: jasmine.Spy<(component: typeof UnsavedChangesDialogComponent, config?: MatDialogConfig) => MatDialogRef>;
- let modalAiService: ModalAiService;
-
- beforeEach(async () => {
- prepareBeforeTest();
-
- modalAiService = TestBed.inject(ModalAiService);
- checkSearchAvailabilitySpy = spyOn(TestBed.inject(SearchAiService), 'checkSearchAvailability');
- notificationService = TestBed.inject(NotificationService);
- spyOn(notificationService, 'showError');
- queryInput = await loader.getHarness(MatInputHarness);
- submitButton = fixture.debugElement.query(By.directive(MatButton));
- await queryInput.setValue(query);
- const inputElement = fixture.debugElement.query(By.directive(MatInput)).nativeElement;
- dialogOpenSpy = spyOn(dialog, 'open').and.returnValue({
- afterClosed: () => of(true)
- } as MatDialogRef);
- submittingTrigger = useButton
- ? () => submitButton.nativeElement.click()
- : () =>
- inputElement.dispatchEvent(
- new KeyboardEvent('keyup', {
- key: 'Enter'
- })
- );
- });
-
- it('should call showError on NotificationService if checkSearchAvailability from SearchAiService returns message', () => {
- const message = 'Some message';
- checkSearchAvailabilitySpy.and.returnValue(message);
- submittingTrigger();
-
- expect(notificationService.showError).toHaveBeenCalledWith(message);
- });
-
- it('should not call showError on NotificationService if checkSearchAvailability from SearchAiService returns empty message', () => {
- checkSearchAvailabilitySpy.and.returnValue('');
- submittingTrigger();
-
- expect(notificationService.showError).not.toHaveBeenCalled();
- });
-
- it('should call checkSearchAvailability on SearchAiService with parameter based on value returned by store', () => {
- submittingTrigger();
-
- expect(checkSearchAvailabilitySpy).toHaveBeenCalledWith(selectionState);
- });
-
- it('should call checkSearchAvailability on SearchAiService with parameter based on value returned by UserPreferencesService', () => {
- component.usedInAiResultsPage = true;
- component.ngOnInit();
- submittingTrigger();
-
- expect(checkSearchAvailabilitySpy).toHaveBeenCalledWith(newSelectionState);
- expect(userPreferencesService.get).toHaveBeenCalledWith('knowledgeRetrievalNodes');
- });
-
- it('should call set on UserPreferencesService with parameter based on value returned by store', () => {
- submittingTrigger();
-
- expect(userPreferencesService.set).toHaveBeenCalledWith('knowledgeRetrievalNodes', JSON.stringify(selectionState));
- });
-
- it('should call set on UserPreferencesService with parameter based on value returned by UserPreferencesService', () => {
- component.usedInAiResultsPage = true;
- component.ngOnInit();
- submittingTrigger();
-
- expect(userPreferencesService.get).toHaveBeenCalledWith('knowledgeRetrievalNodes');
- expect(userPreferencesService.set).toHaveBeenCalledWith('knowledgeRetrievalNodes', JSON.stringify(newSelectionState));
- });
-
- it('should call dispatch on store with correct parameter', () => {
- spyOn(store, 'dispatch');
- submittingTrigger();
-
- expect(store.dispatch).toHaveBeenCalledWith(
- jasmine.objectContaining({
- ...new SearchByTermAiAction({
- searchTerm: query,
- agentId: component.agentId
- })
- })
- );
- expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ToggleAISearchInput('2', 'some query') }));
- });
-
- it('should call dispatch on store with correct parameter if selected agent was changed', async () => {
- spyOn(store, 'dispatch');
- await (
- await loader.getHarness(MatSelectHarness)
- ).clickOptions({
- text: 'HAHR Agent'
- });
- submittingTrigger();
-
- expect(store.dispatch).toHaveBeenCalledWith(
- jasmine.objectContaining({
- ...new SearchByTermAiAction({
- searchTerm: query,
- agentId: '1'
- })
- })
- );
- expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({ ...new ToggleAISearchInput('1', 'some query') }));
- });
-
- it('should call open modal if there was a previous search phrase in url', () => {
- submittingTrigger();
-
- expect(dialogOpenSpy).toHaveBeenCalled();
- });
-
- it('should open Unsaved Changes Modal and run callback successfully', () => {
- const modalAiSpy = spyOn(modalAiService, 'openUnsavedChangesModal').and.callThrough();
-
- fixture.detectChanges();
-
- submittingTrigger();
- expect(modalAiSpy).toHaveBeenCalledWith(jasmine.any(Function));
- });
-
- it('should call reset on queryControl', () => {
- spyOn(component.queryControl, 'reset');
- submittingTrigger();
-
- expect(component.queryControl.reset).toHaveBeenCalled();
- });
- });
- }
-});
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
deleted file mode 100644
index 780dc2ed6..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-input/search-ai-input.component.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/*!
- * Copyright © 2005-2025 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, DestroyRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { TranslatePipe, 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 { Store } from '@ngrx/store';
-import { AiSearchByTermPayload, AppStore, getAppSelection, SearchByTermAiAction, ToggleAISearchInput } from '@alfresco/aca-shared/store';
-import { SelectionState } from '@alfresco/adf-extensions';
-import { MatSelectModule } from '@angular/material/select';
-import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
-import { MatCardModule } from '@angular/material/card';
-import {
- MAT_TOOLTIP_DEFAULT_OPTIONS,
- MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY,
- MatTooltipDefaultOptions,
- MatTooltipModule
-} from '@angular/material/tooltip';
-import { ModalAiService } from '../../../../services/modal-ai.service';
-import { Agent } from '@alfresco/js-api';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-
-const MatTooltipOptions: MatTooltipDefaultOptions = {
- ...MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY(),
- disableTooltipInteractivity: true
-};
-
-@Component({
- imports: [
- CommonModule,
- TranslatePipe,
- MatButtonModule,
- MatIconModule,
- MatFormFieldModule,
- MatInputModule,
- A11yModule,
- FormsModule,
- ReactiveFormsModule,
- MatSelectModule,
- IconComponent,
- AvatarComponent,
- MatCardModule,
- MatTooltipModule
- ],
- selector: 'aca-search-ai-input',
- templateUrl: './search-ai-input.component.html',
- styleUrls: ['./search-ai-input.component.scss'],
- encapsulation: ViewEncapsulation.None,
- providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: MatTooltipOptions }]
-})
-export class SearchAiInputComponent implements OnInit {
- private readonly store = inject>(Store);
- private readonly searchAiService = inject(SearchAiService);
- private readonly notificationService = inject(NotificationService);
- private readonly agentService = inject(AgentService);
- private readonly userPreferencesService = inject(UserPreferencesService);
- private readonly translateService = inject(TranslateService);
- private readonly modalAiService = inject(ModalAiService);
-
- @Input()
- placeholder: string;
-
- @Input()
- agentId: string;
-
- @Input()
- usedInAiResultsPage: boolean;
-
- @Input()
- searchTerm: string;
-
- private readonly storedNodesKey = 'knowledgeRetrievalNodes';
-
- private readonly _agentControl = new FormControl(null);
- private _agents: Agent[] = [];
- private selectedNodesState: SelectionState;
- private readonly _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;
- }
-
- private readonly destroyRef = inject(DestroyRef);
-
- ngOnInit(): void {
- const queryValue = this.usedInAiResultsPage ? '' : this.searchTerm || '';
- this.queryControl.setValue(queryValue);
-
- if (!this.usedInAiResultsPage) {
- this.store
- .select(getAppSelection)
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((selection) => {
- this.selectedNodesState = selection;
- });
- } else {
- this.selectedNodesState = JSON.parse(this.userPreferencesService.get(this.storedNodesKey));
- }
-
- this.agentService
- .getAgents()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe(
- (agents) => {
- this._agents = agents;
-
- 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'))
- );
- }
-
- onSearchSubmit() {
- this.modalAiService.openUnsavedChangesModal(() => this.search());
- }
-
- private search(): void {
- const error = this.searchAiService.checkSearchAvailability(this.selectedNodesState);
- if (error) {
- this.notificationService.showError(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.store.dispatch(new ToggleAISearchInput(this.agentControl.value.id, this.queryControl.value));
- this.queryControl.reset();
- }
- }
-}
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-marked-options.spec.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-marked-options.spec.ts
deleted file mode 100644
index 405a7f33e..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-marked-options.spec.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { searchAiMarkedOptions } from './search-ai-marked-options';
-
-describe('SearchAiMarkedOptions', () => {
- let link = '';
-
- beforeEach(() => {
- link = searchAiMarkedOptions.renderer.link({
- raw: '',
- text: 'Example Link',
- type: 'link',
- href: 'https://example.com',
- title: 'Example',
- tokens: []
- });
- });
-
- it('should return a element', () => {
- expect(link).toContain(' {
- expect(link).toContain('href="https://example.com"');
- });
-
- it('should returned link contain correct target', () => {
- expect(link).toContain('target="_blank"');
- });
-
- it('should returned link contain correct rel', () => {
- expect(link).toContain('rel="noopener noreferrer"');
- });
-
- it('should returned link contain correct title', () => {
- expect(link).toContain('title="Example"');
- });
-
- it('should returned link contain correct text', () => {
- expect(link).toContain('>Example Link');
- });
-
- it('should returned link contain correct title if title is null', () => {
- expect(
- searchAiMarkedOptions.renderer.link({
- raw: '',
- text: 'Example Link',
- type: 'link',
- href: 'https://example.com',
- title: '',
- tokens: []
- })
- ).toContain('title=""');
- });
-});
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-marked-options.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-marked-options.ts
deleted file mode 100644
index 86eb61ece..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-marked-options.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
-import { Tokens } from 'marked/lib/marked';
-
-const renderer = new MarkedRenderer();
-renderer.link = ({ href, title, text }: Tokens.Link): any =>
- `${text}`;
-export const searchAiMarkedOptions: MarkedOptions = {
- renderer
-};
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
deleted file mode 100644
index fb0ff61aa..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.html
+++ /dev/null
@@ -1,126 +0,0 @@
-
- @if (!hasError && agentId) {
-
- }
-
- @if (!hasError) {
-
-
-
- {{ searchQuery }}
-
-
- @if (!loading) {
- @if (!hasAnsweringError) {
-
-
-
-
-
- @if (references$ | async; as refs) {
- @if (refs?.length || hasReferencesLoadingError) {
-
-
-
- {{ 'KNOWLEDGE_RETRIEVAL.SEARCH.RESULTS_PAGE.REFERENCED_DOCUMENTS_HEADER' | translate }}
-
-
- @if (hasReferencesLoadingError) {
-
- {{ 'KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.REFERENCES_LOADING_ERROR' | translate }}
-
-
- } @else {
-
- @for (node of refs; track node.id) {
-
-
-
-
-
- {{ node.name }}
-
-
- }
-
- }
-
- }
- }
-
- } @else {
-
- {{ 'KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.LOADING_ERROR' | translate }}
-
-
- }
- } @else {
-
-
-
- }
-
-
-
- } @else {
-
- }
-
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
deleted file mode 100644
index 84db9bb39..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.scss
+++ /dev/null
@@ -1,223 +0,0 @@
-@use '../../../../ui/mat-selectors' as ms;
-
-.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: var(--theme-white-background);
- 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 {
- padding: 18px 20px;
- display: flex;
- flex-direction: column;
- border: 1px solid var(--adf-theme-foreground-divider-color);
- border-radius: 12px;
- margin: 16px 0 75px;
-
- &-references-container-header {
- padding-left: 8px;
- }
-
- .adf-skeleton {
- position: relative;
- background-image: linear-gradient(
- to left,
- var(--theme-light-grey-1-color) 0%,
- var(--theme-light-grey-2-color) 20%,
- var(--theme-light-grey-3-color) 40%,
- var(--theme-light-grey-1-color) 100%
- );
- background-size: 200%;
- display: inline-block;
- height: 1em;
- overflow: hidden;
- width: 100%;
- margin-bottom: 0.5rem;
- border-radius: 0.25rem;
-
- &-half {
- width: 50%;
- margin-bottom: 8px;
- }
-
- &::after {
- position: absolute;
- inset: 0;
- transform: translateX(-100%);
- background-image: linear-gradient(90deg, rgba(white, 0) 0, rgba(white, 0.2) 20%, rgba(white, 0.5) 60%, rgba(white, 0));
- animation: shimmer 2s infinite;
- content: '';
- }
-
- @keyframes shimmer {
- 100% {
- transform: translateX(100%);
- }
- }
- }
-
- &-error {
- border-color: var(--adf-error-color);
- padding: 32px 18px;
-
- &-message {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- &-regeneration-button {
- background-color: var(--adf-secondary-button-background);
-
- &-icon {
- font-size: 24px;
- height: 24px;
- width: 23px;
- }
- }
- }
- }
-
- &-body {
- &-response {
- margin-bottom: 17px;
- overflow-wrap: break-word;
- white-space: pre-wrap;
-
- &-action {
- width: max-content;
- padding: 0;
-
- mat-icon {
- font-size: 17.25px;
- }
-
- &-regeneration {
- margin-left: 2px;
- margin-right: 2px;
- }
-
- #{ms.$mat-button-touch-target} {
- width: 24px;
- }
- }
-
- table {
- width: 100%;
- border-collapse: collapse;
- box-shadow: 0 2px 4px var(--theme-grey-divider-color);
- border-radius: 4px;
- overflow: hidden;
-
- & th {
- background-color: var(--adf-theme-mat-grey-color-a200);
- text-align: left;
- }
-
- & th,
- & td {
- padding: 16px;
- border-bottom: 1px solid var(--adf-theme-foreground-divider-color);
- }
-
- & tr {
- &:hover {
- background-color: var(--adf-theme-background-hover-color);
- }
-
- &:nth-child(even) {
- background-color: var(--theme-card-background-grey-color);
- }
- }
- }
- }
-
- &-divider {
- margin-top: 9px;
-
- &-error {
- border-color: var(--adf-error-color);
- }
- }
-
- &-references-container {
- padding-right: 8px;
- padding-left: 8px;
-
- &-header {
- margin-top: 16px;
- color: var(--theme-text-light-color);
- font-weight: 400;
- margin-bottom: 3px;
- }
-
- &-loading-error {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 12px;
- padding-bottom: 3px;
- }
-
- &-documents {
- padding-right: 5px;
- padding-top: 5px;
- margin-left: -2px;
- display: flex;
- flex-wrap: wrap;
- gap: 21px;
-
- &-document {
- display: flex;
- flex-direction: row;
- padding-top: 7px;
- padding-bottom: 7px;
-
- &-icon {
- height: auto;
- align-content: center;
- padding-right: 11px;
- }
-
- &-name {
- display: flex;
- flex-direction: column;
- width: 100%;
- justify-content: center;
- }
-
- &:hover {
- text-decoration: underline;
- text-decoration-color: var(--theme-primary-color);
- color: var(--theme-primary-color);
- cursor: pointer;
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.spec.ts b/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.spec.ts
deleted file mode 100644
index 53c50a043..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.spec.ts
+++ /dev/null
@@ -1,755 +0,0 @@
-/*!
- * Copyright © 2005-2025 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 { TestBed, ComponentFixture, tick, fakeAsync } from '@angular/core/testing';
-import { SearchAiResultsComponent } from './search-ai-results.component';
-import { ActivatedRoute, Params, Router } from '@angular/router';
-import { Observable, of, Subject, throwError } from 'rxjs';
-import { MatSnackBarModule } from '@angular/material/snack-bar';
-import { ClipboardService, EmptyContentComponent, UnitTestingUtils, UnsavedChangesGuard, UserPreferencesService } from '@alfresco/adf-core';
-import { MatDialogModule } from '@angular/material/dialog';
-import { AppTestingModule } from '../../../../testing/app-testing.module';
-import { MatIconTestingModule } from '@angular/material/icon/testing';
-import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
-import { ModalAiService } from '../../../../services/modal-ai.service';
-import { delay } from 'rxjs/operators';
-import { AiAnswerEntry, Node, QuestionModel, ResultSetPaging } from '@alfresco/js-api/typings';
-import { SearchAiInputComponent } from '../search-ai-input/search-ai-input.component';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
-import { getAppSelection, getCurrentFolder, ViewNodeAction } from '@alfresco/aca-shared/store';
-import { ContentApiService } from '@alfresco/aca-shared';
-import { ViewerService } from '@alfresco/aca-content/viewer';
-import { DebugElement } from '@angular/core';
-import { MarkdownComponent, MarkdownModule, MARKED_OPTIONS } from 'ngx-markdown';
-import { searchAiMarkedOptions } from './search-ai-marked-options';
-
-const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: { nodesIds: [] } };
-const getAiAnswerEntry = (noAnswer?: boolean): AiAnswerEntry => {
- return {
- entry: {
- answer: noAnswer ? '' : 'Some answer',
- question: 'some question',
- objectReferences: [],
- complete: true
- }
- };
-};
-
-describe('SearchAiResultsComponent', () => {
- const knowledgeRetrievalNodes = '{"isEmpty":"false","nodes":[{"entry":{"id": "someId","isFolder":"true"}}]}';
- let fixture: ComponentFixture;
- let component: SearchAiResultsComponent;
- let userPreferencesService: UserPreferencesService;
- let mockQueryParams = new Subject();
- let modalAiService: ModalAiService;
- let searchAiService: SearchAiService;
- let store: MockStore;
- let viewerService: ViewerService;
- let unsavedChangesGuard: UnsavedChangesGuard;
- let unitTestingUtils: UnitTestingUtils;
- let clipboardService: ClipboardService;
- let contentApiService: ContentApiService;
-
- afterEach(() => {
- store.resetSelectors();
- mockQueryParams = new Subject();
- fixture.destroy();
- });
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [AppTestingModule, SearchAiResultsComponent, MatSnackBarModule, MatDialogModule, MatIconTestingModule, MarkdownModule.forRoot()],
- providers: [
- {
- provide: ActivatedRoute,
- useValue: {
- queryParams: mockQueryParams.asObservable(),
- snapshot: {
- queryParams: { query: 'testQuery' }
- }
- }
- },
- {
- provide: UnsavedChangesGuard,
- useValue: {
- unsaved: false
- }
- },
- provideMockStore()
- ]
- });
-
- fixture = TestBed.createComponent(SearchAiResultsComponent);
- modalAiService = TestBed.inject(ModalAiService);
- searchAiService = TestBed.inject(SearchAiService);
- userPreferencesService = TestBed.inject(UserPreferencesService);
- viewerService = TestBed.inject(ViewerService);
- unsavedChangesGuard = TestBed.inject(UnsavedChangesGuard);
- clipboardService = TestBed.inject(ClipboardService);
- contentApiService = TestBed.inject(ContentApiService);
- store = TestBed.inject(MockStore);
- store.overrideSelector(getAppSelection, {
- nodes: [],
- isEmpty: true,
- count: 0,
- libraries: []
- });
- store.overrideSelector(getCurrentFolder, null);
- spyOn(searchAiService, 'ask').and.returnValue(of(questionMock));
- spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(of([]));
- component = fixture.componentInstance;
- unitTestingUtils = new UnitTestingUtils(fixture.debugElement);
- component.ngOnInit();
- });
-
- describe('query params change', () => {
- const getEmptyContentElement = (): DebugElement => unitTestingUtils.getByDirective(EmptyContentComponent);
-
- it('should perform ai search and sets agents on query params change', () => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- expect(component.searchQuery).toBe('test');
- expect(component.agentId).toBe('agentId1');
- expect(component.hasError).toBeFalse();
- });
-
- it('should throw an error if searchQuery not available', () => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- mockQueryParams.next({ agentId: 'agentId1' });
-
- expect(component.searchQuery).toBe('');
- expect(component.agentId).toBe('agentId1');
- expect(component.hasError).toBeTrue();
- });
-
- it('should not throw an error if selectedNodesState nodes not available', () => {
- spyOn(userPreferencesService, 'get').and.returnValue('{}');
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- expect(component.searchQuery).toBe('test');
- expect(component.agentId).toBe('agentId1');
- expect(component.hasError).toBeFalse();
- });
-
- it('should throw an error if agentId not available', () => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- mockQueryParams.next({ query: 'test' });
-
- expect(component.searchQuery).toBe('test');
- expect(component.agentId).toBe(undefined);
- expect(component.hasError).toBeTrue();
- });
-
- it('should not get query answer and display an error when getAnswer throws error', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValue(throwError(() => 'error').pipe(delay(100)));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(30000);
-
- expect(component.displayedAnswer).toBeUndefined();
- expect(component.hasAnsweringError).toBeTrue();
- expect(component.loading).toBeFalse();
- }));
-
- it('should get query answer and not display an error when getAnswer throws one error and one successful response', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValues(
- throwError(() => 'error'),
- of(getAiAnswerEntry())
- );
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(3000);
-
- expect(component.displayedAnswer).toEqual('Some answer');
- expect(component.hasAnsweringError).toBeFalse();
- }));
-
- it('should display and answer and not display an error when getAnswer throws nine errors and one successful response', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValues(...Array(9).fill(throwError(() => 'error')), of(getAiAnswerEntry()));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(50000);
-
- expect(component.displayedAnswer).toEqual('Some answer');
- expect(component.hasAnsweringError).toBeFalse();
- }));
-
- it('should not display an answer and display an error when getAnswer throws ten errors', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValues(...Array(14).fill(throwError(() => 'error')), of(getAiAnswerEntry(true)));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(30000);
-
- expect(component.displayedAnswer).toBeUndefined();
- expect(component.hasAnsweringError).toBeTrue();
- expect(component.loading).toBeFalse();
- }));
-
- it('should not display answer and display an error if received AiAnswerPaging without answer ten times', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValues(...Array(10).fill(of(getAiAnswerEntry(true))));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(30000);
-
- expect(component.displayedAnswer).toBeUndefined();
- expect(component.hasAnsweringError).toBeTrue();
- expect(component.loading).toBeFalse();
- }));
-
- it('should not display error and display and answer if received AiAnswerPaging without answer nine times and with answer one time', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValues(...Array(9).fill(of(getAiAnswerEntry(true))), of(getAiAnswerEntry()));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(30000);
-
- expect(component.displayedAnswer).toEqual('Some answer');
- expect(component.hasAnsweringError).toBeFalse();
- }));
-
- it('should render empty content when there are not selected nodes', () => {
- mockQueryParams.next({
- query: 'test',
- agentId: 'agentId1'
- });
-
- fixture.detectChanges();
- expect(getEmptyContentElement()).not.toBeNull();
- });
-
- it('should not render empty content when there are selected nodes', () => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- mockQueryParams.next({
- query: 'test',
- agentId: 'agentId1'
- });
-
- fixture.detectChanges();
- expect(getEmptyContentElement()).toBeNull();
- });
-
- describe('when queryAnswer already exists', () => {
- it('should not re-run search when returning from viewer', fakeAsync(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValue(of(getAiAnswerEntry()));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- expect(searchAiService.getAnswer).toHaveBeenCalledTimes(1);
-
- mockQueryParams.next({ query: 'test', agentId: 'agentId1', location: 'viewer' });
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- expect(searchAiService.getAnswer).toHaveBeenCalledTimes(1);
- }));
- });
-
- describe('when query params contains location', () => {
- let params: Params;
-
- beforeEach(() => {
- params = {
- query: 'test',
- agentId: 'agentId1',
- location: 'some-location'
- };
- });
-
- it('should not render search ai input container', () => {
- mockQueryParams.next(params);
-
- fixture.detectChanges();
- expect(unitTestingUtils.getByDirective(SearchAiInputComponent)).toBeNull();
- });
-
- it('should not render empty content', () => {
- mockQueryParams.next({
- location: 'some-location'
- });
-
- fixture.detectChanges();
- expect(getEmptyContentElement()).toBeNull();
- });
-
- it('should not display search query', () => {
- mockQueryParams.next(params);
-
- fixture.detectChanges();
- expect(unitTestingUtils.getByDataAutomationId('aca-search-ai-results-query').nativeElement.textContent.trim()).toBe('');
- });
-
- it('should not call searchAiService.ask', () => {
- mockQueryParams.next(params);
-
- fixture.detectChanges();
- expect(searchAiService.ask).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('skeleton loader', () => {
- const getSkeletonElementsLength = (): number => {
- return fixture.nativeElement.querySelectorAll('.adf-skeleton').length;
- };
-
- beforeEach(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- });
-
- it('should display skeleton when loading is true', () => {
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- component.performAiSearch();
- fixture.detectChanges();
-
- expect(component.loading).toBeTrue();
- expect(getSkeletonElementsLength()).toBe(3);
- });
-
- it('should not display skeleton when loading is false', fakeAsync(() => {
- spyOn(searchAiService, 'getAnswer').and.returnValue(of(getAiAnswerEntry()));
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- component.performAiSearch();
- tick(30000);
-
- expect(component.loading).toBeFalse();
- expect(getSkeletonElementsLength()).toBe(0);
- }));
- });
-
- describe('Unsaved Changes Modal', () => {
- beforeEach(() => {
- spyOn(userPreferencesService, 'get').and.returnValue('true');
- });
-
- it('should open Unsaved Changes Modal and run callback successfully', () => {
- const modalAiSpy = spyOn(modalAiService, 'openUnsavedChangesModal').and.callThrough();
-
- spyOn(searchAiService, 'getAnswer').and.returnValue(of(getAiAnswerEntry()));
-
- fixture.detectChanges();
-
- unitTestingUtils.getByDataAutomationId('aca-search-ai-results-regeneration-button').nativeElement.click();
- expect(modalAiSpy).toHaveBeenCalledWith(jasmine.any(Function));
- expect(component.displayedAnswer).toEqual('Some answer');
- });
- });
-
- describe('Markdown', () => {
- const getMarkdown = (): MarkdownComponent => unitTestingUtils.getByDirective(MarkdownComponent)?.componentInstance;
-
- const removeTabs = (answer: string): string =>
- answer
- .split('\n')
- .map((line) => line.trim())
- .join('\n');
-
- let queryParams: Params;
- let getAnswerSpyAnd: jasmine.SpyAnd<(questionId: string) => Observable>;
- let answerEntry: AiAnswerEntry;
-
- beforeEach(() => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- queryParams = {
- query: 'test',
- agentId: 'agentId1'
- };
- getAnswerSpyAnd = spyOn(searchAiService, 'getAnswer').and;
- answerEntry = getAiAnswerEntry();
- });
-
- it('should have correct marked options', () => {
- expect(fixture.debugElement.injector.get(MARKED_OPTIONS)).toBe(searchAiMarkedOptions);
- });
-
- it('should be rendered when answer is loaded successfully', fakeAsync(() => {
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
-
- tick(3000);
- fixture.detectChanges();
- expect(getMarkdown()).toBeTruthy();
- }));
-
- it('should not be rendered when answer loading is failed', fakeAsync(() => {
- getAnswerSpyAnd.returnValue(throwError(() => 'error').pipe(delay(100)));
- mockQueryParams.next(queryParams);
-
- tick(30000);
- fixture.detectChanges();
- expect(getMarkdown()).toBeUndefined();
- }));
-
- it('should have assigned mermaid to true', fakeAsync(() => {
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
-
- tick(3000);
- fixture.detectChanges();
- expect(getMarkdown().mermaid).toBeTrue();
- }));
-
- it('should have assigned katex to true', fakeAsync(() => {
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
-
- tick(3000);
- fixture.detectChanges();
- expect(getMarkdown().katex).toBeTrue();
- }));
-
- it('should have assigned correct data', fakeAsync(() => {
- const answer = '#### Some title\n\nSome description';
- answerEntry.entry.answer = answer;
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
-
- tick(3000);
- fixture.detectChanges();
- expect(getMarkdown().data).toEqual(answer);
- }));
-
- it('should have assigned correct data when answer contains mermaids', fakeAsync(() => {
- answerEntry.entry.answer =
- 'First example:\\n\\n```mermaid\\ngraph LR\\n animal --> dog\\n animal --> cat\\n```\\n\\n' +
- 'Second example:\\n\\n```mermaid\\ngraph LR\\n animal[label="Animal"] --> dog[label="Dog"]\\n animal[label="Animal"] --> cat[label="Cat"]\\n```\\n\\n';
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
-
- tick(3000);
- fixture.detectChanges();
- expect(removeTabs(getMarkdown().data)).toEqual(
- removeTabs(`First example:\\n\\n\`\`\`mermaid
- \\ngraph LR\\n animal --> dog\\n animal --> cat\\n
- \`\`\`\\n\\nSecond example:\\n\\n\`\`\`mermaid
- \\ngraph LR\\n animal[Animal] --> dog[Dog]\\n animal[Animal] --> cat[Cat]\\n
- \`\`\`\\n\\n`)
- );
- }));
-
- it('should have assigned correct data when answer contains latex', fakeAsync(() => {
- answerEntry.entry.answer = '\n\n### Mathematical Formula\n\n```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx\n```';
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
-
- tick(3000);
- fixture.detectChanges();
- expect(getMarkdown().data).toEqual('\n\n### Mathematical Formula\n\n$$f(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx$$');
- }));
-
- it('should set source code tooltip for mermaid when answer contains mermaid', fakeAsync(() => {
- answerEntry.entry.answer =
- 'First example:\\n\\n```mermaid\\ngraph LR\\n animal --> dog\\n animal --> cat\\n```\\n\\n' +
- 'Second example:\\n\\n```mermaid\\ngraph LR\\n animal[label="Animal"] --> dog[label="Dog"]\\n animal[label="Animal"] --> cat[label="Cat"]\\n```\\n\\n';
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
- tick(3000);
- fixture.detectChanges();
- const elements = [document.createElement('div'), document.createElement('div')];
- spyOn(fixture.nativeElement, 'querySelectorAll').withArgs('.mermaid').and.returnValue(elements).and.returnValue([]);
-
- getMarkdown().ready.emit();
- expect(elements[0].title).toBe('```mermaid\\ngraph LR\\n animal --> dog\\n animal --> cat\\n```');
- expect(elements[1].title).toBe(
- '```mermaid\\ngraph LR\\n animal[label="Animal"] --> dog[label="Dog"]\\n animal[label="Animal"] --> cat[label="Cat"]\\n```'
- );
- }));
-
- it('should set source code tooltip for mermaid when answer contains latex', fakeAsync(() => {
- answerEntry.entry.answer =
- '\n\n### Mathematical Formula 1\n\n```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx\n```' +
- '\n\n### Mathematical Formula 2\n\n```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{5 (pi i Ixi x} dx\n```';
- getAnswerSpyAnd.returnValues(
- throwError(() => 'error'),
- of(answerEntry)
- );
- mockQueryParams.next(queryParams);
- tick(3000);
- fixture.detectChanges();
- const elements = [document.createElement('div'), document.createElement('div')];
- spyOn(fixture.nativeElement, 'querySelectorAll').withArgs('.katex').and.returnValue(elements).and.returnValue([]);
-
- getMarkdown().ready.emit();
- expect(elements[0].title).toBe('```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{2 (pi i Ixi x} dx\n```');
- expect(elements[1].title).toBe('```latex\nf(x) = Vint_{-\\infty}^{\\infty} \\hat{f}(lxi) e^{5 (pi i Ixi x} dx\n```');
- }));
- });
-
- describe('References', () => {
- let documentElement: DebugElement;
-
- const nodeId = 'someId';
- const secondNodeId = 'someId1';
- const url = 'some-url';
-
- const node1 = { id: nodeId, isFolder: true } as Node;
- const node2 = { id: secondNodeId, isFolder: false } as Node;
- const nodeError = throwError(() => 'error');
-
- const toSearchResult = (nodes: Node[]): Observable =>
- of({
- list: {
- entries: nodes.map((node) => ({ entry: node }))
- }
- } as ResultSetPaging).pipe(delay(50));
-
- const setupReferencesTest = (objectIds: string[] = [], searchResult: Observable[] = [], hasNodeId = false) => {
- spyOn(contentApiService, 'search').and.returnValues(...searchResult);
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
-
- const answer = getAiAnswerEntry();
- answer.entry.objectReferences = objectIds.map((id) => ({
- objectId: `sourceId__${id}`,
- nodeId: hasNodeId ? `plain-${id}` : undefined,
- references: []
- }));
-
- spyOn(searchAiService, 'getAnswer').and.returnValues(
- throwError(() => 'error'),
- of(answer)
- );
-
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
-
- tick(3051);
- fixture.detectChanges();
-
- tick(51);
- fixture.detectChanges();
-
- documentElement = unitTestingUtils.getByDataAutomationId(`aca-search-ai-results-${nodeId}-document`);
- spyOnProperty(TestBed.inject(Router), 'url').and.returnValue(url);
- };
-
- it('should dispatch ViewNodeAction on store when clicked', fakeAsync(() => {
- spyOn(store, 'dispatch');
- setupReferencesTest([nodeId], [toSearchResult([node1])]);
-
- documentElement.nativeElement.click();
- expect(store.dispatch).toHaveBeenCalledWith(
- jasmine.objectContaining({
- ...new ViewNodeAction(nodeId, {
- location: url
- })
- })
- );
- }));
-
- it('should dispatch ViewNodeAction on store when pressed enter', fakeAsync(() => {
- spyOn(store, 'dispatch');
- setupReferencesTest([nodeId], [toSearchResult([node1])]);
-
- documentElement.nativeElement.dispatchEvent(
- new KeyboardEvent('keyup', {
- key: 'Enter'
- })
- );
- expect(store.dispatch).toHaveBeenCalledWith(
- jasmine.objectContaining({
- ...new ViewNodeAction(nodeId, {
- location: url
- })
- })
- );
- }));
-
- it('should assign nodes ids to customNodesOrder for ViewerService', fakeAsync(() => {
- let nodesOrder: string[];
- spyOnProperty(viewerService, 'customNodesOrder', 'set').and.callFake((passedNodesOrder) => (nodesOrder = passedNodesOrder));
- setupReferencesTest([nodeId], [toSearchResult([node1])]);
-
- expect(nodesOrder).toEqual([nodeId]);
- }));
-
- it('should call set on userPreferencesService with correct parameters', fakeAsync(() => {
- spyOn(userPreferencesService, 'set');
- setupReferencesTest([nodeId], [toSearchResult([node1])]);
-
- expect(userPreferencesService.set).toHaveBeenCalledWith('aiReferences', JSON.stringify([nodeId]));
- }));
-
- it('should display answer and reference nodes when all are fetched', fakeAsync(() => {
- setupReferencesTest([nodeId, secondNodeId], [toSearchResult([node1, node2])]);
-
- expect(component.displayedAnswer).toEqual('Some answer');
- expect(component.hasReferencesLoadingError).toBeFalse();
- expect(unitTestingUtils.getByDataAutomationId(`aca-search-ai-results-${nodeId}-document`)).toBeTruthy();
- expect(unitTestingUtils.getByDataAutomationId(`aca-search-ai-results-${secondNodeId}-document`)).toBeTruthy();
- }));
-
- it('should use nodeId when available when fetching references', fakeAsync(() => {
- setupReferencesTest([nodeId], [toSearchResult([node1])], true);
- expect(contentApiService.search).toHaveBeenCalledWith(
- jasmine.objectContaining({
- query: {
- query: `ID:"plain-${nodeId}"`,
- language: 'afts'
- }
- })
- );
- }));
-
- describe('Reload References', () => {
- const getReloadButton = (): DebugElement =>
- unitTestingUtils.getByDataAutomationId('aca-search-ai-response-container-body-references-container-retry-references-loading-button');
-
- it('should set hasReferencesLoadingError and display reload button when not all references are fetched', fakeAsync(() => {
- setupReferencesTest([nodeId, secondNodeId], [toSearchResult([node1])]);
- const reloadButton = getReloadButton();
-
- expect(component.hasReferencesLoadingError).toBeTrue();
- expect(unitTestingUtils.getByDataAutomationId(`aca-search-ai-results-${nodeId}-document`)).toBeFalsy();
- expect(unitTestingUtils.getByDataAutomationId(`aca-search-ai-results-${secondNodeId}-document`)).toBeFalsy();
- expect(reloadButton).toBeTruthy();
- }));
-
- it('should set hasReferencesLoadingError and display reload button when search request fails', fakeAsync(() => {
- setupReferencesTest([nodeId], [nodeError]);
- const reloadButton = getReloadButton();
-
- expect(component.hasReferencesLoadingError).toBeTrue();
- expect(unitTestingUtils.getByDataAutomationId(`aca-search-ai-results-${nodeId}-document`)).toBeFalsy();
- expect(reloadButton).toBeTruthy();
- }));
-
- it('should call search api when reload references button is clicked', fakeAsync(() => {
- setupReferencesTest([nodeId, secondNodeId], [toSearchResult([node1]), toSearchResult([node1, node2])]);
- getReloadButton().nativeElement.click();
-
- expect(contentApiService.search).toHaveBeenCalledWith(
- jasmine.objectContaining({
- query: {
- query: `ID:"${nodeId}" OR ID:"${secondNodeId}"`,
- language: 'afts'
- }
- })
- );
- }));
-
- it('should not display reload button after references are successfully reloaded', fakeAsync(() => {
- setupReferencesTest([nodeId, secondNodeId], [toSearchResult([]), toSearchResult([node1, node2])]);
- const reloadButton = getReloadButton();
- reloadButton.nativeElement.click();
- tick(51);
- fixture.detectChanges();
-
- expect(getReloadButton()).toBeNull();
- }));
-
- it('should not display reload button when there are no references', fakeAsync(() => {
- setupReferencesTest();
-
- expect(component.hasReferencesLoadingError).toBeFalse();
- expect(getReloadButton()).toBeNull();
- }));
- });
- });
-
- describe('ngOnInit', () => {
- it('should set customNodesOrder on ViewerService', () => {
- spyOn(userPreferencesService, 'get').and.returnValue('["node1", "node2"]');
- let nodesOrder: string[];
- spyOnProperty(viewerService, 'customNodesOrder', 'set').and.callFake((passedNodesOrder) => (nodesOrder = passedNodesOrder));
-
- component.ngOnInit();
-
- expect(nodesOrder).toEqual(['node1', 'node2']);
- });
-
- it('should set unsaved on UnsavedChangesGuard to false when there are no selected nodes', () => {
- mockQueryParams.next({
- query: 'test',
- agentId: 'agentId1'
- });
-
- component.ngOnInit();
- expect(unsavedChangesGuard.unsaved).toBeFalse();
- });
-
- it('should set unsaved on UnsavedChangesGuard to true when there are selected nodes', () => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- mockQueryParams.next({
- query: 'test',
- agentId: 'agentId1'
- });
-
- component.ngOnInit();
- expect(unsavedChangesGuard.unsaved).toBeTrue();
- });
-
- it('should set correct data on unsavedChangesGuard', () => {
- component.ngOnInit();
- expect(unsavedChangesGuard.data).toEqual({
- descriptionText: 'KNOWLEDGE_RETRIEVAL.SEARCH.DISCARD_CHANGES.CONVERSATION_DISCARDED',
- confirmButtonText: 'KNOWLEDGE_RETRIEVAL.SEARCH.DISCARD_CHANGES.DISCARD_CONVERSATION',
- headerText: 'KNOWLEDGE_RETRIEVAL.SEARCH.DISCARD_CHANGES.WARNING',
- maxWidth: 'none'
- });
- });
- });
-
- it('should copy answer to clipboard and show notification on copy button click', () => {
- spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
- spyOn(searchAiService, 'getAnswer').and.returnValue(of(getAiAnswerEntry()));
- spyOn(clipboardService, 'copyContentToClipboard');
-
- mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
- fixture.detectChanges();
-
- const copyButton = unitTestingUtils.getByDataAutomationId('aca-search-ai-results-copying-button').nativeElement;
- copyButton.click();
-
- expect(clipboardService.copyContentToClipboard).toHaveBeenCalledWith(
- component.displayedAnswer,
- 'KNOWLEDGE_RETRIEVAL.SEARCH.RESULTS_PAGE.COPY_MESSAGE'
- );
- });
-});
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
deleted file mode 100644
index 3a5c974e0..000000000
--- a/projects/aca-content/src/lib/components/knowledge-retrieval/search-ai/search-ai-results/search-ai-results.component.ts
+++ /dev/null
@@ -1,332 +0,0 @@
-/*!
- * Copyright © 2005-2025 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, ElementRef, OnInit, ViewEncapsulation, inject } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { PageComponent, PageLayoutComponent, ContentApiService } from '@alfresco/aca-shared';
-import { catchError, delay, filter, finalize, map, retry, shareReplay, switchMap, tap } from 'rxjs/operators';
-import { ClipboardService, EmptyContentComponent, ThumbnailService, UnsavedChangesGuard } 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 { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { from, Observable, of, throwError } 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';
-import { MatCardModule } from '@angular/material/card';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { ModalAiService } from '../../../../services/modal-ai.service';
-import { ViewNodeAction } from '@alfresco/aca-shared/store';
-import { ViewerService } from '@alfresco/aca-content/viewer';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MarkdownModule, MARKED_OPTIONS, provideMarkdown } from 'ngx-markdown';
-import { searchAiMarkedOptions } from './search-ai-marked-options';
-
-@Component({
- imports: [
- CommonModule,
- PageLayoutComponent,
- SearchAiInputContainerComponent,
- TranslatePipe,
- MatIconModule,
- MatButtonModule,
- MatListModule,
- EmptyContentComponent,
- MatCardModule,
- MatTooltipModule,
- MarkdownModule
- ],
- providers: [
- provideMarkdown({
- markedOptions: {
- provide: MARKED_OPTIONS,
- useValue: searchAiMarkedOptions
- }
- })
- ],
- 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 {
- private readonly route = inject(ActivatedRoute);
- private readonly clipboardService = inject(ClipboardService);
- private readonly thumbnailService = inject(ThumbnailService);
- private readonly translateService = inject(TranslateService);
- private readonly unsavedChangesGuard = inject(UnsavedChangesGuard);
- private readonly modalAiService = inject(ModalAiService);
- private readonly viewerService = inject(ViewerService);
- private readonly elementRef = inject(ElementRef);
- private readonly contentApi = inject(ContentApiService);
-
- private static readonly MERMAID_BLOCK_REGEX = /```mermaid([\s\S]*?)```/g;
- private static readonly LATEX_BLOCK_REGEX = /```latex([\s\S]*?)```/g;
-
- references$: Observable = of([]);
-
- private _agentId: string;
- private _hasAnsweringError = false;
- private _hasError = false;
- private _loading = false;
- private _mimeTypeIconsByNodeId: { [key: string]: string } = {};
- private openedViewer = false;
- private _selectedNodesState: SelectionState;
- private _searchQuery = '';
- private queryAnswer: AiAnswer;
- private _displayedAnswer: string;
- private _hasReferencesLoadingError = false;
- private _referencesLoading = false;
-
- 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 searchQuery(): string {
- return this._searchQuery;
- }
-
- get displayedAnswer(): string {
- return this._displayedAnswer;
- }
-
- get hasReferencesLoadingError(): boolean {
- return this._hasReferencesLoadingError;
- }
-
- ngOnInit(): void {
- this.viewerService.customNodesOrder = JSON.parse(this.userPreferencesService.get('aiReferences', '[]'));
- this.route.queryParams
- .pipe(
- filter((params) => {
- const openedViewerPreviously = this.openedViewer;
- this.openedViewer = !!params.location;
- return !this.openedViewer && (!openedViewerPreviously || !this.queryAnswer);
- }),
- takeUntilDestroyed(this.destroyRef)
- )
- .subscribe((params) => {
- this._agentId = params.agentId;
- this._searchQuery = params.query ? decodeURIComponent(params.query) : '';
- const selectedNodesState = this.userPreferencesService.get('knowledgeRetrievalNodes');
- if (!this.searchQuery || !this.agentId || !selectedNodesState) {
- this._hasError = true;
- return;
- }
- this._selectedNodesState = JSON.parse(selectedNodesState);
- this.performAiSearch();
- });
- super.ngOnInit();
-
- this.unsavedChangesGuard.unsaved = this.route.snapshot?.queryParams?.query?.length > 0 && !this.hasError;
- this.unsavedChangesGuard.data = {
- descriptionText: 'KNOWLEDGE_RETRIEVAL.SEARCH.DISCARD_CHANGES.CONVERSATION_DISCARDED',
- confirmButtonText: 'KNOWLEDGE_RETRIEVAL.SEARCH.DISCARD_CHANGES.DISCARD_CONVERSATION',
- headerText: 'KNOWLEDGE_RETRIEVAL.SEARCH.DISCARD_CHANGES.WARNING',
- maxWidth: 'none'
- };
- }
-
- copyResponseToClipboard(): void {
- this.clipboardService.copyContentToClipboard(
- this.queryAnswer.answer,
- this.translateService.instant('KNOWLEDGE_RETRIEVAL.SEARCH.RESULTS_PAGE.COPY_MESSAGE')
- );
- }
-
- checkUnsavedChangesAndSearch(): void {
- this.modalAiService.openUnsavedChangesModal(() => this.performAiSearch());
- }
-
- performAiSearch(): void {
- this._loading = true;
- this._hasAnsweringError = false;
-
- this.searchAiService
- .ask({
- question: this.searchQuery,
- nodeIds: this._selectedNodesState?.nodes?.length ? this._selectedNodesState.nodes.map((node) => node.entry.id) : [],
- agentId: this._agentId
- })
- .pipe(
- switchMap((response) => this.searchAiService.getAnswer(response.questionId)),
- tap((response) => {
- if (!response.entry?.answer) {
- throw new Error();
- }
- this.queryAnswer = response.entry;
- this._displayedAnswer = this.preprocessMarkdownFormat(response.entry.answer);
- this.loadReferences();
- }),
- retry({
- delay: (error: Error, retryCount) => this.aiSearchRetryDelay(error, retryCount)
- }),
- finalize(() => {
- this._loading = false;
- }),
- takeUntilDestroyed(this.destroyRef)
- )
- .subscribe({
- error: () => (this._hasAnsweringError = true)
- });
- }
-
- openFile(id: string): void {
- this.store.dispatch(
- new ViewNodeAction(id, {
- location: this.router.url
- })
- );
- }
-
- addSourceCodeTooltips(): void {
- this.setTooltip(SearchAiResultsComponent.MERMAID_BLOCK_REGEX, '.mermaid');
- this.setTooltip(SearchAiResultsComponent.LATEX_BLOCK_REGEX, '.katex');
- }
-
- loadReferences(): void {
- if (this._referencesLoading) {
- return;
- }
-
- this._referencesLoading = true;
-
- this.references$ = this.fetchReferences(this.queryAnswer).pipe(
- tap((nodes) => this.updateNodes(nodes)),
- finalize(() => {
- this._referencesLoading = false;
- }),
- shareReplay({ bufferSize: 1, refCount: true })
- );
- }
-
- private setTooltip(codeBlockRegexp: RegExp, targetElementsSelector: string): void {
- const codeBlocks = [...this.queryAnswer.answer.matchAll(codeBlockRegexp)].map((match) => match[0].trim());
- const elements: HTMLElement[] = this.elementRef.nativeElement.querySelectorAll(targetElementsSelector);
- for (let i = 0; i < elements.length; i++) {
- elements[i].title = codeBlocks[i];
- }
- }
-
- private aiSearchRetryDelay(error: Error, retryCount: number): Observable {
- this._hasAnsweringError = false;
- const delayBetweenRetries = 3000;
- const maxRetries = 9;
-
- if (retryCount > maxRetries) {
- this._hasAnsweringError = true;
- return throwError(() => error);
- }
-
- return of(undefined).pipe(delay(delayBetweenRetries));
- }
-
- private preprocessMarkdownFormat(answer: string): string {
- return this.transformLatex(this.transformMermaid(answer));
- }
-
- private transformMermaid(answer: string): string {
- return answer.replace(SearchAiResultsComponent.MERMAID_BLOCK_REGEX, (_mermaidBlockRegex, blockContent: string) => {
- const transformedLines = blockContent.split('\n').map((line) => {
- const label = 'label="';
- while (line.includes(label)) {
- const labelIndex = line.indexOf(label);
- const start = labelIndex + label.length;
- const end = line.indexOf('"', start);
- line = line.slice(0, labelIndex) + line.slice(start, end) + line.slice(end + 1);
- }
- return line;
- });
-
- return `\`\`\`mermaid\n${transformedLines.join('\n')}\n\`\`\``;
- });
- }
-
- private transformLatex(answer: string): string {
- return answer.replace(SearchAiResultsComponent.LATEX_BLOCK_REGEX, (_, latexContent: string) => `$$${latexContent.trim()}$$`);
- }
-
- private fetchReferences(answer?: AiAnswer): Observable {
- this._hasReferencesLoadingError = false;
-
- const objectIds = answer?.objectReferences?.map((reference) => reference.nodeId ?? reference.objectId.split('__')[1]);
-
- if (!objectIds?.length) {
- return of([]);
- }
-
- const query = objectIds.map((id) => `ID:"${id}"`).join(' OR ');
-
- return from(
- this.contentApi.search({
- query: {
- query,
- language: 'afts'
- }
- })
- ).pipe(
- map((result) => {
- const nodes = result.list.entries.map((entry) => entry.entry as Node);
- if (nodes.length !== objectIds.length) {
- this._hasReferencesLoadingError = true;
- return [];
- }
- return nodes;
- }),
- catchError(() => {
- this._hasReferencesLoadingError = true;
- return of([]);
- })
- );
- }
-
- private updateNodes(nodes: Node[]): void {
- const nodesIds: string[] = [];
- nodes.forEach((node) => {
- nodesIds.push(node.id);
- this._mimeTypeIconsByNodeId[node.id] = this.thumbnailService.getMimeTypeIcon(node.content?.mimeType);
- });
- this.viewerService.customNodesOrder = nodesIds;
- this.userPreferencesService.set('aiReferences', JSON.stringify(nodesIds));
- }
-}
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 36260ff64..5d178d61c 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,16 +1,11 @@
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 87de75402..4857e4ffc 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
@@ -44,7 +44,6 @@ import {
} from '@alfresco/adf-core';
import { DocumentListDirective } from '../../directives/document-list.directive';
import { TranslatePipe } from '@ngx-translate/core';
-import { SearchAiInputContainerComponent } from '../knowledge-retrieval/search-ai/search-ai-input-container/search-ai-input-container.component';
import { DocumentListComponent } from '@alfresco/adf-content-services';
@Component({
@@ -58,7 +57,6 @@ import { DocumentListComponent } from '@alfresco/adf-content-services';
PageLayoutComponent,
TranslatePipe,
ToolbarComponent,
- SearchAiInputContainerComponent,
EmptyContentComponent,
DynamicColumnComponent,
DocumentListComponent,
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 e020a42e8..f137e58f5 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,6 +1,5 @@
-
+