[ACS-8210] Agent basic details popup (#3942)

* [ACS-8210] Agent basic details popup

* [ACS-8210] Agent basic details popup - review fixes

---------

Co-authored-by: Aleksander Sklorz <aleksander.sklorz@hyland.com>
This commit is contained in:
jacekpluta
2024-07-31 10:18:48 +02:00
committed by Aleksander Sklorz
parent 28880a0de6
commit 5bfa638684
12 changed files with 412 additions and 112 deletions

View File

@@ -26,8 +26,10 @@
[attr.data-automation-id]="'aca-agents-button-agent-' + agent.id"
[value]="agent">
<div class="aca-agents-button-menu-list-agent-content">
<adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar>
{{ agent.name }}
<adf-avatar [src]="agent?.avatar" [initials]="initialsByAgentId[agent.id]"></adf-avatar>
<span class="aca-agents-button-menu-list-agent-content-name" [matTooltip]="agent.name" [matTooltipPosition]="'right'">
{{ agent.name }}
</span>
</div>
</mat-list-option>
</mat-selection-list>

View File

@@ -46,7 +46,14 @@ aca-agents-button.aca-agents-button {
&-content {
display: flex;
align-items: baseline;
align-items: center;
&-name {
width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
adf-avatar {
@@ -54,6 +61,10 @@ aca-agents-button.aca-agents-button {
margin-bottom: 2px;
padding-left: 1px;
padding-top: 1px;
.adf-avatar__image {
cursor: pointer;
}
}
}
}

View File

@@ -25,7 +25,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AgentsButtonComponent } from './agents-button.component';
import { AgentService, ContentTestingModule, SearchAiService } from '@alfresco/adf-content-services';
import { AgentPaging } from '@alfresco/js-api';
import { AgentWithAvatar } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { By } from '@angular/platform-browser';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
@@ -38,12 +38,13 @@ 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';
describe('AgentsButtonComponent', () => {
let component: AgentsButtonComponent;
let fixture: ComponentFixture<AgentsButtonComponent>;
let agents$: Subject<AgentPaging>;
let agentPaging: AgentPaging;
let agents$: Subject<AgentWithAvatar[]>;
let agentsWithAvatar: AgentWithAvatar[];
let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>;
let selectionState: SelectionState;
let store: MockStore;
@@ -61,26 +62,22 @@ describe('AgentsButtonComponent', () => {
fixture = TestBed.createComponent(AgentsButtonComponent);
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
agents$ = new Subject<AgentPaging>();
agents$ = new Subject<AgentWithAvatar[]>();
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
agentPaging = {
list: {
entries: [
{
entry: {
id: '1',
name: 'HR Agent'
}
},
{
entry: {
id: '2',
name: 'Policy Agent'
}
}
]
agentsWithAvatar = [
{
id: '1',
name: 'HR Agent',
description: 'Test 1',
avatar: undefined
},
{
id: '2',
name: 'Policy Agent',
description: 'Test 2',
avatar: undefined
}
};
];
checkSearchAvailabilitySpy = spyOn(TestBed.inject(SearchAiService), 'checkSearchAvailability');
selectionState = {
nodes: [],
@@ -93,30 +90,55 @@ describe('AgentsButtonComponent', () => {
});
describe('Button', () => {
let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef<any>>;
beforeEach(() => {
const notificationService = TestBed.inject(NotificationService);
notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
});
it('should be rendered if any agents are loaded', () => {
agents$.next(agentPaging);
agents$.next(agentsWithAvatar);
fixture.detectChanges();
expect(getAgentsButton()).toBeTruthy();
});
it('should get agents on component init', () => {
agents$.next(agentsWithAvatar);
component.ngOnInit();
expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' });
expect(component.agents).toEqual(agentsWithAvatar);
expect(notificationServiceSpy).not.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', () => {
agentPaging.list.entries = [];
agents$.next(agentPaging);
agentsWithAvatar = [];
agents$.next(agentsWithAvatar);
fixture.detectChanges();
expect(getAgentsButton()).toBeFalsy();
});
it('should have correct label', () => {
agents$.next(agentPaging);
agents$.next(agentsWithAvatar);
fixture.detectChanges();
expect(getAgentsButton().textContent.trim()).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.AGENTS_BUTTON.LABEL');
});
it('should contain stars icon', () => {
agents$.next(agentPaging);
agents$.next(agentsWithAvatar);
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.aca-agents-menu-button adf-icon')).componentInstance.value).toBe('adf:colored-stars-ai');
@@ -135,7 +157,7 @@ describe('AgentsButtonComponent', () => {
: new KeyboardEvent(eventName, {
key: 'Enter'
});
agents$.next(agentPaging);
agents$.next(agentsWithAvatar);
});
it('should display notification if checkSearchAvailability from SearchAiService returns message', () => {
@@ -195,7 +217,7 @@ describe('AgentsButtonComponent', () => {
beforeEach(() => {
loader = TestbedHarnessEnvironment.loader(fixture);
agents$.next(agentPaging);
agents$.next(agentsWithAvatar);
fixture.detectChanges();
checkSearchAvailabilitySpy.and.returnValue('');
const button = getAgentsButton();

View File

@@ -33,12 +33,13 @@ import { takeUntil } from 'rxjs/operators';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule, MatSelectionListChange } from '@angular/material/list';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Agent } from '@alfresco/js-api';
import { AgentWithAvatar } from '@alfresco/js-api';
import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
standalone: true,
imports: [CommonModule, MatMenuModule, MatListModule, TranslateModule, AvatarComponent, IconComponent],
imports: [CommonModule, MatMenuModule, MatListModule, TranslateModule, AvatarComponent, IconComponent, MatTooltipModule],
selector: 'aca-agents-button',
templateUrl: './agents-button.component.html',
styleUrls: ['./agents-button.component.scss'],
@@ -50,12 +51,12 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
data: { trigger: string };
private selectedNodesState: SelectionState;
private _agents: Agent[] = [];
private _agents: AgentWithAvatar[] = [];
private onDestroy$ = new Subject<void>();
private _disabled = true;
private _initialsByAgentId: { [key: string]: string } = {};
get agents(): Agent[] {
get agents(): AgentWithAvatar[] {
return this._agents;
}
@@ -71,8 +72,8 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
private store: Store<AppStore>,
private notificationService: NotificationService,
private searchAiService: SearchAiService,
private translateService: TranslateService,
private agentService: AgentService
private agentService: AgentService,
private translateService: TranslateService
) {}
ngOnInit(): void {
@@ -82,12 +83,13 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
.subscribe((selection) => {
this.selectedNodesState = selection;
});
this.agentService
.getAgents()
.pipe(takeUntil(this.onDestroy$))
.subscribe(
(paging) => {
this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry);
(agents) => {
this._agents = agents;
if (this.agents.length) {
this._initialsByAgentId = this.agents.reduce((initials, agent) => {
const words = agent.name.split(' ').filter((word) => !word.match(/[^a-zA-Z]+/g));

View File

@@ -1,27 +1,49 @@
<mat-select
[formControl]="agentControl"
class="aca-search-ai-input-agent-select"
panelClass="aca-search-ai-input-agent-select-options aca-search-ai-input-agent-select-agents"
data-automation-id="aca-search-ai-agents-select"
<div class="aca-search-ai-input-agent-container">
<mat-select
[formControl]="agentControl"
class="aca-search-ai-input-agent-select"
panelClass="aca-search-ai-input-agent-select-options aca-search-ai-input-agent-select-agents"
data-automation-id="aca-search-ai-agents-select"
[hideSingleSelectionIndicator]="true">
<mat-select-trigger class="aca-search-ai-input-agent-select-displayed-value">
<adf-avatar
[initials]="initialsByAgentId[agentControl.value.id]"
size="26px">
</adf-avatar>
{{ agentControl.value.name }}
</mat-select-trigger>
<mat-option
*ngFor="let agent of agents"
[value]="agent"
class="aca-search-ai-input-agent-select-options-option"
[attr.data-automation-id]="'aca-search-ai-input-agent-' + agent.id">
<div class="aca-search-ai-input-agent-select-options-option-content">
<adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar>
{{ agent.name }}
</div>
</mat-option>
</mat-select>
<mat-select-trigger class="aca-search-ai-input-agent-select-displayed-value">
<adf-avatar
[src]="agentControl.value?.avatar"
[initials]="initialsByAgentId[agentControl.value?.id]"
size="26px">
</adf-avatar>
<span class="aca-search-ai-input-agent-select-displayed-value-text">{{ agentControl.value?.name }}</span>
</mat-select-trigger>
<mat-option
*ngFor="let agent of agents"
[value]="agent"
class="aca-search-ai-input-agent-select-options-option"
[attr.data-automation-id]="'aca-search-ai-input-agent-' + agent.id"><div class="aca-search-ai-input-agent-select-options-option-content">
<adf-avatar [src]="agent?.avatar" [initials]="initialsByAgentId[agent.id]"></adf-avatar>
<span class="aca-search-ai-input-agent-select-options-option-content-text" [matTooltip]="agent.name" [matTooltipPosition]="'right'">{{ agent.name }}</span>
</div>
</mat-option>
</mat-select>
<div class="aca-search-ai-input-agent-popup-hover-card">
<mat-card class="aca-search-ai-input-agent-popup-hover-card-container">
<mat-card-title class="aca-search-ai-input-agent-popup-hover-card-container-title">
<adf-avatar
[initials]="initialsByAgentId[agentControl.value?.id]"
[src]="agentControl.value?.avatar"
size="50px">
</adf-avatar>
<span class="aca-search-ai-input-agent-popup-hover-card-container-title-name"
[matTooltipPosition]="'right'"
[matTooltip]="agentControl.value?.name">
{{ agentControl.value?.name }}
</span>
</mat-card-title>
<mat-card-content class="aca-search-ai-input-agent-popup-hover-card-container-content">
{{ agentControl.value?.description }}
</mat-card-content>
</mat-card>
</div>
</div>
<input
class="aca-search-ai-input-text"
matInput

View File

@@ -6,6 +6,7 @@ aca-search-ai-input {
align-items: center;
.aca-search-ai-input-text {
margin-top: 4px;
flex: 1;
font-size: 20px;
margin-right: 167px;
@@ -54,13 +55,24 @@ aca-search-ai-input {
font-size: 15px;
margin-right: 26px;
#{$mat-select-trigger} {
height: auto;
margin-top: 4px;
}
&:focus {
outline: -webkit-focus-ring-color auto 1px;
}
&-displayed-value {
display: flex;
align-items: baseline;
align-items: center;
&-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
adf-avatar {
@@ -68,6 +80,10 @@ aca-search-ai-input {
margin-right: 6px;
padding-top: 1px;
padding-bottom: 3px;
.adf-avatar__image {
cursor: pointer;
}
}
}
}
@@ -81,14 +97,83 @@ aca-search-ai-input {
&-content {
display: flex;
align-items: baseline;
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;
}
}
}

View File

@@ -29,8 +29,8 @@ import { By } from '@angular/platform-browser';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { AgentService, ContentTestingModule, SearchAiService } from '@alfresco/adf-content-services';
import { getAppSelection, SearchByTermAiAction } from '@alfresco/aca-shared/store';
import { of } from 'rxjs';
import { NodeEntry } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { AgentWithAvatar, NodeEntry } from '@alfresco/js-api';
import { FormControlDirective } from '@angular/forms';
import { DebugElement } from '@angular/core';
import { AvatarComponent, IconComponent, NotificationService, UserPreferencesService } from '@alfresco/adf-core';
@@ -42,6 +42,22 @@ 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';
const agentWithAvatarList: AgentWithAvatar[] = [
{
id: '1',
name: 'HR Agent',
description: 'Test 1',
avatar: undefined
},
{
id: '2',
name: 'Policy Agent',
description: 'Test 2',
avatar: undefined
}
];
describe('SearchAiInputComponent', () => {
let component: SearchAiInputComponent;
@@ -49,6 +65,7 @@ describe('SearchAiInputComponent', () => {
let loader: HarnessLoader;
let selectionState: SelectionState;
let store: MockStore;
let agents$: Subject<AgentWithAvatar[]>;
const prepareBeforeTest = (): void => {
selectionState = {
@@ -73,34 +90,19 @@ describe('SearchAiInputComponent', () => {
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
loader = TestbedHarnessEnvironment.loader(fixture);
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(
of({
list: {
entries: [
{
entry: {
id: '1',
name: 'HR Agent'
}
},
{
entry: {
id: '2',
name: 'Policy Agent'
}
}
]
}
})
);
agents$ = new Subject<AgentWithAvatar[]>();
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
prepareBeforeTest();
});
describe('Agent select box', () => {
let selectElement: DebugElement;
let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef<any>>;
beforeEach(() => {
selectElement = fixture.debugElement.query(By.directive(MatSelect));
const notificationService = TestBed.inject(NotificationService);
notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
});
it('should have assigned formControl', () => {
@@ -111,8 +113,26 @@ describe('SearchAiInputComponent', () => {
expect(selectElement.componentInstance.hideSingleSelectionIndicator).toBeTrue();
});
it('should get agents on init', () => {
agents$.next(agentWithAvatarList);
component.ngOnInit();
expect(component.agents).toEqual(agentWithAvatarList);
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 () => {
expect(await (await loader.getHarness(MatSelectHarness)).getValueText()).toBe('PA Policy Agent');
agents$.next(agentWithAvatarList);
expect(await (await loader.getHarness(MatSelectHarness)).getValueText()).toBe('PAPolicy Agent');
const avatar = selectElement.query(By.directive(AvatarComponent))?.componentInstance;
expect(avatar.initials).toBe('PA');
expect(avatar.size).toBe('26px');
@@ -126,6 +146,7 @@ describe('SearchAiInputComponent', () => {
.componentInstance;
beforeEach(async () => {
agents$.next(agentWithAvatarList);
const selectHarness = await loader.getHarness(MatSelectHarness);
await selectHarness.open();
options = await selectHarness.getOptions();
@@ -136,8 +157,8 @@ describe('SearchAiInputComponent', () => {
});
it('should have correct agent names', async () => {
expect(await options[0].getText()).toBe('HA HR Agent');
expect(await options[1].getText()).toBe('PA Policy Agent');
expect(await options[0].getText()).toBe('HAHR Agent');
expect(await options[1].getText()).toBe('PAPolicy Agent');
});
it('should display avatar for each agent', () => {
@@ -157,6 +178,7 @@ describe('SearchAiInputComponent', () => {
beforeEach(() => {
queryInput = fixture.debugElement.query(By.directive(MatInput));
agents$.next(agentWithAvatarList);
});
it('should have assigned formControl', () => {
@@ -181,6 +203,7 @@ describe('SearchAiInputComponent', () => {
beforeEach(async () => {
submitButton = fixture.debugElement.query(By.directive(MatButton));
queryInput = await loader.getHarness(MatInputHarness);
agents$.next(agentWithAvatarList);
});
it('should be disabled by default', () => {
@@ -324,7 +347,7 @@ describe('SearchAiInputComponent', () => {
await (
await loader.getHarness(MatSelectHarness)
).clickOptions({
text: 'HA HR Agent'
text: 'HAHR Agent'
});
submittingTrigger();

View File

@@ -38,8 +38,20 @@ import { AiSearchByTermPayload, AppStore, getAppSelection, SearchByTermAiAction
import { takeUntil } from 'rxjs/operators';
import { SelectionState } from '@alfresco/adf-extensions';
import { MatSelectModule } from '@angular/material/select';
import { Agent } from '@alfresco/js-api';
import { AgentWithAvatar } from '@alfresco/js-api';
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';
const MatTooltipOptions: MatTooltipDefaultOptions = {
...MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY(),
disableTooltipInteractivity: true
};
@Component({
standalone: true,
@@ -55,12 +67,15 @@ import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
ReactiveFormsModule,
MatSelectModule,
IconComponent,
AvatarComponent
AvatarComponent,
MatCardModule,
MatTooltipModule
],
selector: 'aca-search-ai-input',
templateUrl: './search-ai-input.component.html',
styleUrls: ['./search-ai-input.component.scss'],
encapsulation: ViewEncapsulation.None
encapsulation: ViewEncapsulation.None,
providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: MatTooltipOptions }]
})
export class SearchAiInputComponent implements OnInit, OnDestroy {
@Input()
@@ -75,18 +90,18 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
private readonly storedNodesKey = 'knowledgeRetrievalNodes';
private _agentControl = new FormControl<Agent>(null);
private _agents: Agent[] = [];
private _agentControl = new FormControl<AgentWithAvatar>(null);
private _agents: AgentWithAvatar[] = [];
private onDestroy$ = new Subject<void>();
private selectedNodesState: SelectionState;
private _queryControl = new FormControl('');
private _initialsByAgentId: { [key: string]: string } = {};
get agentControl(): FormControl<Agent> {
get agentControl(): FormControl<AgentWithAvatar> {
return this._agentControl;
}
get agents(): Agent[] {
get agents(): AgentWithAvatar[] {
return this._agents;
}
@@ -103,8 +118,8 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
private searchAiService: SearchAiService,
private notificationService: NotificationService,
private agentService: AgentService,
private translateService: TranslateService,
private userPreferencesService: UserPreferencesService
private userPreferencesService: UserPreferencesService,
private translateService: TranslateService
) {}
ngOnInit(): void {
@@ -118,13 +133,14 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
} else {
this.selectedNodesState = JSON.parse(this.userPreferencesService.get(this.storedNodesKey));
}
this.agentService
.getAgents()
.pipe(takeUntil(this.onDestroy$))
.subscribe(
(paging) => {
this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry);
this.agentControl.setValue(this.agents.find((agent) => agent.id === this.agentId));
(agents) => {
this._agents = agents;
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] || ''}`;

View File

@@ -61,7 +61,7 @@
width: 100%;
&-response {
margin-bottom: 17px;
margin: 17px 0;
padding-left: 6px;
padding-right: 5px;
overflow-wrap: break-word;
@@ -98,7 +98,7 @@
padding-left: 8px;
&-header {
margin-top: 8px;
margin-top: 16px;
color: var(--theme-text-light-color);
font-weight: 400;
margin-bottom: 3px;

View File

@@ -0,0 +1,102 @@
/*!
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { SearchAiResultsComponent } from './search-ai-results.component';
import { ActivatedRoute, Params } from '@angular/router';
import { Subject } from 'rxjs';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { UserPreferencesService } from '@alfresco/adf-core';
import { AppTestingModule } from '../../../../testing/app-testing.module';
import { MatDialogModule } from '@angular/material/dialog';
describe('SearchAiResultsComponent', () => {
const knowledgeRetrievalNodes = '{"isEmpty":"false","nodes":[{"entry":{"id": "someId","isFolder":"true"}}]}';
let fixture: ComponentFixture<SearchAiResultsComponent>;
let component: SearchAiResultsComponent;
let userPreferencesService: UserPreferencesService;
let mockQueryParams = new Subject<Params>();
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule, SearchAiResultsComponent, MatSnackBarModule, MatDialogModule],
providers: [
{
provide: ActivatedRoute,
useValue: {
queryParams: mockQueryParams.asObservable()
}
}
]
});
fixture = TestBed.createComponent(SearchAiResultsComponent);
userPreferencesService = TestBed.inject(UserPreferencesService);
component = fixture.componentInstance;
component.ngOnInit();
});
afterEach(() => {
mockQueryParams = new Subject<Params>();
fixture.destroy();
});
describe('query params change', () => {
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 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).toBeTrue();
});
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();
});
});
});

View File

@@ -23,10 +23,17 @@
*/
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { PageComponent, PageLayoutComponent, ToolbarActionComponent, ToolbarComponent } from '@alfresco/aca-shared';
import { finalize, switchMap, takeUntil } from 'rxjs/operators';
import { ClipboardService, EmptyContentComponent, ThumbnailService, ToolbarModule, UserPreferencesService } from '@alfresco/adf-core';
import {
AvatarComponent,
ClipboardService,
EmptyContentComponent,
ThumbnailService,
ToolbarModule,
UserPreferencesService
} from '@alfresco/adf-core';
import { AiAnswer, Node } from '@alfresco/js-api';
import { CommonModule } from '@angular/common';
import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component';
@@ -37,6 +44,8 @@ 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';
@Component({
standalone: true,
@@ -51,7 +60,10 @@ import { MatListModule } from '@angular/material/list';
MatIconModule,
MatButtonModule,
MatListModule,
EmptyContentComponent
EmptyContentComponent,
MatCardModule,
AvatarComponent,
MatTooltipModule
],
selector: 'aca-search-ai-results',
templateUrl: './search-ai-results.component.html',
@@ -66,7 +78,7 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
private _loading = true;
private _mimeTypeIconsByNodeId: { [key: string]: string } = {};
private _nodes: Node[] = [];
private selectedNodesState: SelectionState;
private _selectedNodesState: SelectionState;
private _searchQuery = '';
private _queryAnswer: AiAnswer;
@@ -114,11 +126,12 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
}
ngOnInit(): void {
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params) => {
this._agentId = params.agentId;
this._searchQuery = params.query ? decodeURIComponent(params.query) : '';
this.selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes'));
if (!this.searchQuery || !this.selectedNodesState?.nodes?.length || !this.agentId) {
this._selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes'));
if (!this.searchQuery || !this._selectedNodesState?.nodes?.length || !this.agentId) {
this._hasError = true;
return;
}
@@ -144,7 +157,7 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
this.searchAiService
.ask({
question: this.searchQuery,
nodeIds: this.selectedNodesState.nodes.map((node) => node.entry.id)
nodeIds: this._selectedNodesState.nodes.map((node) => node.entry.id)
})
.pipe(
switchMap((response) => this.searchAiService.getAnswer(response.questionId)),

View File

@@ -28,6 +28,7 @@ $blue-save-button-background: #1f74db;
$blue-checkbox-background: rgb(10, 96, 206);
$blue-active-table-row: rgb(10, 96, 206, 0.24);
$black-heading: #4e4c4c;
$light-grey-content: #4b5563;
$theme-dropdown-background: darken($background-color, 5%);
$theme-dropdown-background-hover: darken($background-color, 10%);
$grey-divider: rgba(0, 0, 0, 0.22);
@@ -77,6 +78,7 @@ $defaults: (
--theme-blue-checkbox-color: $blue-checkbox-background,
--theme-blue-active-table-row-color: $blue-active-table-row,
--theme-heading-color: $black-heading,
--theme-content-color: $light-grey-content,
--theme-dropdown-color: $theme-dropdown-background,
--theme-dropdown-background-hover: $theme-dropdown-background-hover,
--theme-grey-divider-color: $grey-divider,