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
index 08d723f6c..ad900b03b 100644
--- 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
@@ -39,6 +39,7 @@ 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';
describe('AgentsButtonComponent', () => {
let component: AgentsButtonComponent;
@@ -113,6 +114,16 @@ describe('AgentsButtonComponent', () => {
expect(notificationServiceSpy).not.toHaveBeenCalled();
});
+ it('should run detectChanges when getting the agents', () => {
+ const changeDetectorRef2 = fixture.debugElement.injector.get(ChangeDetectorRef);
+ const detectChangesSpy = spyOn(changeDetectorRef2.constructor.prototype, 'detectChanges');
+
+ component.ngOnInit();
+ agents$.next(agentsWithAvatar);
+
+ expect(detectChangesSpy).toHaveBeenCalled();
+ });
+
it('should show notification error on getAgents error', () => {
agents$.error('error');
component.ngOnInit();
@@ -291,10 +302,22 @@ describe('AgentsButtonComponent', () => {
expect(getAvatar('2')).toBeTruthy();
});
- it('should assign correct initials to each avatar for each agent', () => {
+ 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 initials to each avatar for each agent with single section name', () => {
+ const newAgentWithAvatarList = [
+ { ...agentsWithAvatar[0], name: 'Adam' },
+ { ...agentsWithAvatar[1], name: 'Bob' }
+ ];
+ agents$.next(newAgentWithAvatarList);
+ fixture.detectChanges();
+
+ 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
index 7090a20b8..94b1bc47d 100644
--- 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
@@ -22,7 +22,7 @@
* from Hyland Software. If not, see .
*/
-import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
+import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SelectionState } from '@alfresco/adf-extensions';
import { Store } from '@ngrx/store';
@@ -73,7 +73,8 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
private notificationService: NotificationService,
private searchAiService: SearchAiService,
private agentService: AgentService,
- private translateService: TranslateService
+ private translateService: TranslateService,
+ private cd: ChangeDetectorRef
) {}
ngOnInit(): void {
@@ -90,10 +91,12 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
.subscribe(
(agents) => {
this._agents = 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] || ''}`;
+ initials[agent.id] = `${words[0][0]}${words[1]?.[0] || ''}`;
return initials;
}, {});
}
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
index 4e508168a..c8538456e 100644
--- 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
@@ -185,6 +185,18 @@ describe('SearchAiInputComponent', () => {
expect(getAvatarForAgent('1').initials).toBe('HA');
expect(getAvatarForAgent('2').initials).toBe('PA');
});
+
+ it('should assign correct initials to each avatar for each agent with single section name', () => {
+ const newAgentWithAvatarList = [
+ { ...agentWithAvatarList[0], name: 'Adam' },
+ { ...agentWithAvatarList[1], name: 'Bob' }
+ ];
+ agents$.next(newAgentWithAvatarList);
+ fixture.detectChanges();
+
+ expect(getAvatarForAgent('1').initials).toBe('A');
+ expect(getAvatarForAgent('2').initials).toBe('B');
+ });
});
});
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
index e61413af0..848e2858c 100644
--- 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
@@ -147,7 +147,7 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
this.agentControl.setValue(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] || ''}`;
+ initials[agent.id] = `${words[0][0]}${words[1]?.[0] || ''}`;
return initials;
}, {});
},
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
index f4107f499..cab6019bb 100644
--- 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
@@ -22,10 +22,10 @@
* from Hyland Software. If not, see .
*/
-import { TestBed, ComponentFixture } from '@angular/core/testing';
+import { TestBed, ComponentFixture, tick, fakeAsync } from '@angular/core/testing';
import { SearchAiResultsComponent } from './search-ai-results.component';
import { ActivatedRoute, Params } from '@angular/router';
-import { of, Subject } from 'rxjs';
+import { of, Subject, throwError } from 'rxjs';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { UserPreferencesService } from '@alfresco/adf-core';
import { MatDialogModule } from '@angular/material/dialog';
@@ -37,12 +37,12 @@ import { ModalAiService } from '../../../../services/modal-ai.service';
import { delay } from 'rxjs/operators';
import { AiAnswer, AiAnswerPaging, QuestionModel } from '@alfresco/js-api/typings';
-const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: '' };
+const questionMock: QuestionModel = { question: 'test', questionId: 'testId', restrictionQuery: { nodesIds: [] } };
const aiAnswerMock: AiAnswer = { answer: 'Some answer', questionId: 'some id', references: [] };
-const getAiAnswerPaging = (withEntry: boolean): AiAnswerPaging => {
+const getAiAnswerPaging = (withEntry: boolean, noAnswer?: boolean): AiAnswerPaging => {
return {
list: {
- entries: withEntry ? [{ entry: { answer: 'Some answer', questionId: 'some id', references: [] } }] : [],
+ entries: withEntry ? [{ entry: { answer: noAnswer ? '' : 'Some answer', questionId: 'some id', references: [] } }] : [],
pagination: { hasMoreItems: false, maxItems: 0, totalItems: 0, skipCount: 0 }
}
};
@@ -128,6 +128,75 @@ describe('SearchAiResultsComponent', () => {
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.queryAnswer).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(getAiAnswerPaging(true)));
+ mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
+
+ tick(3000);
+
+ expect(component.queryAnswer).toEqual({ answer: 'Some answer', questionId: 'some id', references: [] });
+ 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(getAiAnswerPaging(true)));
+ mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
+
+ tick(50000);
+
+ expect(component.queryAnswer).toEqual({ answer: 'Some answer', questionId: 'some id', references: [] });
+ 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(getAiAnswerPaging(true)));
+ mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
+
+ tick(30000);
+
+ expect(component.queryAnswer).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(getAiAnswerPaging(true, true))));
+ mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
+
+ tick(30000);
+
+ expect(component.queryAnswer).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(getAiAnswerPaging(true, true))), of(getAiAnswerPaging(true)));
+ mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
+
+ tick(30000);
+
+ expect(component.queryAnswer).toEqual({ answer: 'Some answer', questionId: 'some id', references: [] });
+ expect(component.hasAnsweringError).toBeFalse();
+ }));
});
describe('skeleton loader', () => {
@@ -149,18 +218,18 @@ describe('SearchAiResultsComponent', () => {
expect(getSkeletonElementsLength()).toBe(3);
});
- it('should not display skeleton when loading is false', () => {
+ it('should not display skeleton when loading is false', fakeAsync(() => {
mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
spyOn(searchAiService, 'ask').and.returnValue(of(questionMock));
spyOn(searchAiService, 'getAnswer').and.returnValue(of(getAiAnswerPaging(false)));
component.performAiSearch();
- fixture.detectChanges();
+ tick(30000);
expect(component.loading).toBeFalse();
expect(getSkeletonElementsLength()).toBe(0);
- });
+ }));
});
describe('Unsaved Changes Modal', () => {
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
index 05a9f22f9..dd735fcd5 100644
--- 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
@@ -25,7 +25,7 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PageComponent, PageLayoutComponent, ToolbarActionComponent, ToolbarComponent } from '@alfresco/aca-shared';
-import { finalize, switchMap, takeUntil } from 'rxjs/operators';
+import { concatMap, delay, finalize, retryWhen, skipWhile, switchMap, takeUntil } from 'rxjs/operators';
import {
AvatarComponent,
ClipboardService,
@@ -40,7 +40,7 @@ import { CommonModule } from '@angular/common';
import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NodesApiService } from '@alfresco/adf-content-services';
-import { forkJoin } from 'rxjs';
+import { forkJoin, Observable, of, throwError } from 'rxjs';
import { SelectionState } from '@alfresco/adf-extensions';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
@@ -168,17 +168,23 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
performAiSearch(): void {
this._loading = true;
+
this.searchAiService
.ask({
question: this.searchQuery,
- nodeIds: this._selectedNodesState?.nodes?.length ? this._selectedNodesState.nodes.map((node) => node.entry.id) : []
+ nodeIds: this._selectedNodesState?.nodes?.length ? this._selectedNodesState.nodes.map((node) => node.entry.id) : [],
+ agentId: this._agentId
})
.pipe(
switchMap((response) => this.searchAiService.getAnswer(response.questionId)),
switchMap((response) => {
+ if (!response.list.entries[0].entry?.answer) {
+ return throwError((e) => e);
+ }
this._queryAnswer = response.list.entries[0].entry;
return forkJoin(this.queryAnswer.references.map((reference) => this.nodesApiService.getNode(reference.referenceId)));
}),
+ retryWhen((errors: Observable) => this.aiSearchRetryWhen(errors)),
finalize(() => (this._loading = false)),
takeUntil(this.onDestroy$)
)
@@ -192,4 +198,23 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
() => (this._hasAnsweringError = true)
);
}
+
+ private aiSearchRetryWhen(errors: Observable): Observable {
+ this._hasAnsweringError = false;
+ const delayBetweenRetries = 3000;
+ const maxRetries = 9;
+
+ return errors.pipe(
+ skipWhile(() => this.hasAnsweringError),
+ delay(delayBetweenRetries),
+ concatMap((e, index) => {
+ if (index === maxRetries) {
+ this._hasAnsweringError = true;
+ this._loading = false;
+ return throwError(e);
+ }
+ return of(null);
+ })
+ );
+ }
}