mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-09-17 14:21:14 +00:00
[ACS-8399] Integrate all changes with backend (#4076)
* [ACS-8399] Integrate all changes with backend * [ACS-8399] Integrate all changes with backend - review fixes
This commit is contained in:
committed by
Aleksander Sklorz
parent
5b8d1d4088
commit
867874cf16
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -22,7 +22,7 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}, {});
|
||||
}
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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;
|
||||
}, {});
|
||||
},
|
||||
|
@@ -22,10 +22,10 @@
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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', () => {
|
||||
|
@@ -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<Error>) => 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<Error>): Observable<Error> {
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user