[ACS-8210] Agent basic details popup

This commit is contained in:
Aleksander Sklorz
2024-07-10 13:32:57 +02:00
committed by Jacek Pluta
parent 17a7697e5e
commit 88dd1e76ed
10 changed files with 185 additions and 41 deletions

View File

@@ -607,6 +607,7 @@
"KNOWLEDGE_RETRIEVAL": { "KNOWLEDGE_RETRIEVAL": {
"SEARCH": { "SEARCH": {
"RESULTS_PAGE": { "RESULTS_PAGE": {
"AGENT": "Agent",
"QUERY_INPUT_PLACEHOLDER": "Would you like to ask anything else?", "QUERY_INPUT_PLACEHOLDER": "Would you like to ask anything else?",
"REFERENCED_DOCUMENTS_HEADER": "Referenced documents", "REFERENCED_DOCUMENTS_HEADER": "Referenced documents",
"REGENERATION_BUTTON_LABEL": "Regenerate", "REGENERATION_BUTTON_LABEL": "Regenerate",

View File

@@ -26,7 +26,9 @@
[value]="agent"> [value]="agent">
<div class="aca-agents-button-menu-list-agent-content"> <div class="aca-agents-button-menu-list-agent-content">
<adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar> <adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar>
<span class="aca-agents-button-menu-list-agent-content-name">
{{ agent.name }} {{ agent.name }}
</span>
</div> </div>
</mat-list-option> </mat-list-option>
</mat-selection-list> </mat-selection-list>

View File

@@ -45,7 +45,14 @@ aca-agents-button.aca-agents-button {
&-content { &-content {
display: flex; display: flex;
align-items: baseline; align-items: center;
&-name {
width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }
adf-avatar { adf-avatar {

View File

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

View File

@@ -8,7 +8,7 @@
[initials]="initialsByAgentId[agentControl.value.id]" [initials]="initialsByAgentId[agentControl.value.id]"
size="26px"> size="26px">
</adf-avatar> </adf-avatar>
{{ agentControl.value.name }} {{ agentControl.value?.name }}
</mat-select-trigger> </mat-select-trigger>
<mat-option <mat-option
*ngFor="let agent of agents" *ngFor="let agent of agents"
@@ -16,7 +16,7 @@
class="aca-search-ai-input-agent-select-options-option"> class="aca-search-ai-input-agent-select-options-option">
<div class="aca-search-ai-input-agent-select-options-option-content"> <div class="aca-search-ai-input-agent-select-options-option-content">
<adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar> <adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar>
{{ agent.name }} <span class="aca-search-ai-input-agent-select-options-option-content-text">{{ agent.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>

View File

@@ -49,7 +49,7 @@ aca-search-ai-input {
&-displayed-value { &-displayed-value {
display: flex; display: flex;
align-items: baseline; align-items: center;
} }
adf-avatar { adf-avatar {
@@ -71,6 +71,13 @@ aca-search-ai-input {
&-content { &-content {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
&-text {
width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }
adf-avatar { adf-avatar {

View File

@@ -38,7 +38,7 @@ import { AiSearchByTermPayload, AppStore, getAppSelection, SearchByTermAiAction
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { SelectionState } from '@alfresco/adf-extensions'; import { SelectionState } from '@alfresco/adf-extensions';
import { MatSelectModule } from '@angular/material/select'; 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 { AgentService, SearchAiService } from '@alfresco/adf-content-services';
@Component({ @Component({
@@ -75,18 +75,18 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
private readonly storedNodesKey = 'knowledgeRetrievalNodes'; private readonly storedNodesKey = 'knowledgeRetrievalNodes';
private _agentControl = new FormControl<Agent>(null); private _agentControl = new FormControl<AgentWithAvatar>(null);
private _agents: Agent[] = []; private _agents: AgentWithAvatar[] = [];
private onDestroy$ = new Subject<void>(); private onDestroy$ = new Subject<void>();
private selectedNodesState: SelectionState; private selectedNodesState: SelectionState;
private _queryControl = new FormControl(''); private _queryControl = new FormControl('');
private _initialsByAgentId: { [key: string]: string } = {}; private _initialsByAgentId: { [key: string]: string } = {};
get agentControl(): FormControl<Agent> { get agentControl(): FormControl<AgentWithAvatar> {
return this._agentControl; return this._agentControl;
} }
get agents(): Agent[] { get agents(): AgentWithAvatar[] {
return this._agents; return this._agents;
} }
@@ -103,8 +103,8 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
private searchAiService: SearchAiService, private searchAiService: SearchAiService,
private notificationService: NotificationService, private notificationService: NotificationService,
private agentService: AgentService, private agentService: AgentService,
private translateService: TranslateService, private userPreferencesService: UserPreferencesService,
private userPreferencesService: UserPreferencesService private translateService: TranslateService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -118,13 +118,14 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
} else { } else {
this.selectedNodesState = JSON.parse(this.userPreferencesService.get(this.storedNodesKey)); this.selectedNodesState = JSON.parse(this.userPreferencesService.get(this.storedNodesKey));
} }
this.agentService this.agentService
.getAgents() .getAgents()
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe( .subscribe(
(paging) => { (agents: AgentWithAvatar[]) => {
this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry); this._agents = agents;
this.agentControl.setValue(this.agents.find((agent) => agent.id === this.agentId)); this.agentControl.setValue(agents.find((agent) => agent.id === this.agentId));
this._initialsByAgentId = this.agents.reduce((initials, agent) => { this._initialsByAgentId = this.agents.reduce((initials, agent) => {
const words = agent.name.split(' ').filter((word) => !word.match(/[^a-zA-Z]+/g)); 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] || ''}`;

View File

@@ -18,6 +18,26 @@
<div <div
class="aca-search-ai-response-container" class="aca-search-ai-response-container"
[class.aca-search-ai-response-container-error]="hasAnsweringError"> [class.aca-search-ai-response-container-error]="hasAnsweringError">
<div class="aca-search-ai-response-container-body-agent">
<adf-avatar
src="{{ agent?.avatar }}"
size="26px">
</adf-avatar>
<span class="aca-search-ai-response-container-body-agent-name">{{ agent?.name }}</span>
<mat-card class="aca-search-ai-response-container-body-agent-hover-card">
<mat-card-title class="aca-search-ai-response-container-body-agent-hover-card-title">
<adf-avatar
src="{{ agent?.avatar }}"
size="26px">
</adf-avatar>
<span [matTooltipPosition]="'right'" matTooltip="{{ agent?.name }}">{{ agent?.name }}</span>
</mat-card-title>
<mat-card-content class="aca-search-ai-response-container-body-agent-hover-card-content">
<div>{{ agent?.description }}</div>
</mat-card-content>
</mat-card>
</div>
<mat-divider class="aca-search-ai-response-container-body-divider"></mat-divider>
<ng-container *ngIf="!loading"> <ng-container *ngIf="!loading">
<div <div
class="aca-search-ai-response-container-body" class="aca-search-ai-response-container-body"

View File

@@ -52,8 +52,89 @@
&-body { &-body {
width: 100%; width: 100%;
&-agent {
display: flex;
align-items: center;
position: relative;
height: 32px;
padding: 8px 0 4px 4px;
width: fit-content;
img {
margin-right: 6px;
border-radius: 50%;
height: 32px;
width: 32px;
min-width: 32px;
min-height: 32px;
display: inline-block;
}
&-name {
font-weight: 500;
}
&-hover-card {
display: none;
width: 315px;
height: fit-content;
margin-top: 20px;
position: absolute;
top: 16px;
left: -16px;
z-index: 1;
border-radius: 12px;
&:hover {
display: block;
}
&-title:has(img) {
display: flex;
align-items: center;
font-size: 20px;
font-weight: 500;
img {
height: 50px;
width: 50px;
min-width: 50px;
min-height: 50px;
margin-right: 12px;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 8px;
}
}
&-content {
padding-top: 4px;
padding-right: 8px;
display: flex;
color: var(--theme-heading-color);
max-height: 150px;
overflow-y: auto;
cursor: default;
text-align: justify;
text-justify: inter-word;
}
}
&:hover {
cursor: pointer;
.aca-search-ai-response-container-body-agent-hover-card {
display: block;
}
}
}
&-response { &-response {
margin-bottom: 17px; margin: 17px 0;
padding-left: 6px; padding-left: 6px;
padding-right: 5px; padding-right: 5px;
overflow-wrap: break-word; overflow-wrap: break-word;
@@ -85,7 +166,7 @@
padding-left: 8px; padding-left: 8px;
&-header { &-header {
margin-top: 8px; margin-top: 16px;
color: var(--theme-text-light-color); color: var(--theme-text-light-color);
font-weight: 400; font-weight: 400;
margin-bottom: 3px; margin-bottom: 3px;

View File

@@ -23,20 +23,29 @@
*/ */
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; 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 { PageComponent, PageLayoutComponent, ToolbarActionComponent, ToolbarComponent } from '@alfresco/aca-shared';
import { finalize, switchMap, takeUntil } from 'rxjs/operators'; import { finalize, switchMap, takeUntil } from 'rxjs/operators';
import { ClipboardService, EmptyContentComponent, ThumbnailService, ToolbarModule, UserPreferencesService } from '@alfresco/adf-core'; import {
import { AiAnswer, Node } from '@alfresco/js-api'; AvatarComponent,
ClipboardService,
EmptyContentComponent,
ThumbnailService,
ToolbarModule,
UserPreferencesService
} from '@alfresco/adf-core';
import { AgentWithAvatar, AiAnswer, Node } from '@alfresco/js-api';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component'; import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NodesApiService } from '@alfresco/adf-content-services'; import { AgentService, NodesApiService, SearchAiService } from '@alfresco/adf-content-services';
import { forkJoin } from 'rxjs'; import { forkJoin, of } from 'rxjs';
import { SelectionState } from '@alfresco/adf-extensions'; import { SelectionState } from '@alfresco/adf-extensions';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { MatCardModule } from '@angular/material/card';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({ @Component({
standalone: true, standalone: true,
@@ -51,7 +60,10 @@ import { MatListModule } from '@angular/material/list';
MatIconModule, MatIconModule,
MatButtonModule, MatButtonModule,
MatListModule, MatListModule,
EmptyContentComponent EmptyContentComponent,
MatCardModule,
AvatarComponent,
MatTooltipModule
], ],
selector: 'aca-search-ai-results', selector: 'aca-search-ai-results',
templateUrl: './search-ai-results.component.html', templateUrl: './search-ai-results.component.html',
@@ -70,6 +82,8 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
private _searchQuery = ''; private _searchQuery = '';
private _queryAnswer: AiAnswer; private _queryAnswer: AiAnswer;
agent: AgentWithAvatar;
get agentId(): string { get agentId(): string {
return this._agentId; return this._agentId;
} }
@@ -108,21 +122,31 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
private thumbnailService: ThumbnailService, private thumbnailService: ThumbnailService,
private nodesApiService: NodesApiService, private nodesApiService: NodesApiService,
private userPreferencesService: UserPreferencesService, private userPreferencesService: UserPreferencesService,
private translateService: TranslateService private translateService: TranslateService,
private agentService: AgentService,
protected searchAiService: SearchAiService
) { ) {
super(); super();
} }
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => { this.route.queryParams
.pipe(
switchMap((params) => {
this._agentId = params.agentId; this._agentId = params.agentId;
this._searchQuery = params.query ? decodeURIComponent(params.query) : ''; this._searchQuery = params.query ? decodeURIComponent(params.query) : '';
this.selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes')); this.selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes'));
if (!this.searchQuery || !this.selectedNodesState?.nodes?.length || !this.agentId) { if (!this.searchQuery || !this.selectedNodesState?.nodes?.length || !this.agentId) {
this._hasError = true; this._hasError = true;
return; return of([]);
} }
this.performAiSearch(); this.performAiSearch();
return this.agentService.agentsList$;
}),
takeUntil(this.onDestroy$)
)
.subscribe((agents: AgentWithAvatar[]) => {
this.agent = agents.find((agent) => agent.id === this._agentId);
}); });
super.ngOnInit(); super.ngOnInit();
} }