Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tock Studio] Replay of a full dialog + Dialogs view refacto + Search by dialog id + Link to observability trace + NlpStats on dialogs actions + Pass connector and footnotes to faq creation #1799

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
5 changes: 5 additions & 0 deletions bot/admin/web/src/app/analytics/analytics-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DialogsComponent } from './dialogs/dialogs.component';
import { UsersComponent } from './users/users.component';
import { PreferencesComponent } from './preferences/preferences.component';
import { SatisfactionComponent } from './satisfaction/satisfaction.component';
import { DialogComponent } from './dialog/dialog.component';

const routes: Routes = [
{
Expand Down Expand Up @@ -40,6 +41,10 @@ const routes: Routes = [
path: 'dialogs',
component: DialogsComponent
},
{
path: 'dialogs/:namespace/:applicationId/:dialogId',
component: DialogComponent
},
{
path: 'users',
component: UsersComponent
Expand Down
12 changes: 9 additions & 3 deletions bot/admin/web/src/app/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import {
NbRadioModule,
NbToggleModule,
NbIconModule,
NbFormFieldModule
NbFormFieldModule,
NbPopoverModule
} from '@nebular/theme';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ChartComponent } from './chart/chart.component';
Expand All @@ -59,6 +60,8 @@ import { ActivateSatisfactionComponent } from './satisfaction/activate-satisfact
import { SatisfactionDetailsComponent } from './satisfaction/satisfaction-details/satisfaction-details.component';
import { AnalyticsRoutingModule } from './analytics-routing.module';
import { DialogsListComponent } from './dialogs/dialogs-list/dialogs-list.component';
import { DialogComponent } from './dialog/dialog.component';
import { DialogsListFiltersComponent } from './dialogs/dialogs-list/dialogs-list-filters/dialogs-list-filters.component';

@NgModule({
schemas: [NO_ERRORS_SCHEMA],
Expand Down Expand Up @@ -95,7 +98,8 @@ import { DialogsListComponent } from './dialogs/dialogs-list/dialogs-list.compon
NbIconModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts')
})
}),
NbPopoverModule
],
declarations: [
AnalyticsTabsComponent,
Expand All @@ -110,7 +114,9 @@ import { DialogsListComponent } from './dialogs/dialogs-list/dialogs-list.compon
SatisfactionComponent,
ActivateSatisfactionComponent,
SatisfactionDetailsComponent,
DialogsListComponent
DialogsListComponent,
DialogsListFiltersComponent,
DialogComponent
],
exports: [],
providers: [AnalyticsService]
Expand Down
37 changes: 22 additions & 15 deletions bot/admin/web/src/app/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ import { TestPlan } from '../test/model/test';
import { DialogReport } from '../shared/model/dialog-data';
import { ApplicationDialogFlow, DialogFlowRequest } from './flow/flow';
import { UserAnalyticsPreferences } from './preferences/UserAnalyticsPreferences';
import { StorySearchQuery } from "../bot/model/story";
import {RatingReportQueryResult} from "./satisfaction/satisfaction-details/RatingReportQueryResult";

import { StorySearchQuery } from '../bot/model/story';
import { RatingReportQueryResult } from './satisfaction/satisfaction-details/RatingReportQueryResult';

@Injectable()
export class AnalyticsService {
Expand Down Expand Up @@ -104,8 +103,8 @@ export class AnalyticsService {
return this.rest.get(`/dialog/${applicationId}/${dialogId}`, DialogReport.fromJSON);
}

dialogWithIntentFilter(applicationId: string, dialogId: string, intentsToHide: string[]) : Observable<DialogReport> {
return this.rest.post(`/dialog/${applicationId}/${dialogId}/satisfaction`,intentsToHide, DialogReport.fromJSON);
dialogWithIntentFilter(applicationId: string, dialogId: string, intentsToHide: string[]): Observable<DialogReport> {
return this.rest.post(`/dialog/${applicationId}/${dialogId}/satisfaction`, intentsToHide, DialogReport.fromJSON);
}

getTestPlansByNamespaceAndNlpModel(): Observable<TestPlan[]> {
Expand Down Expand Up @@ -134,30 +133,38 @@ export class AnalyticsService {
}

isActiveSatisfactionByBot(): Observable<Boolean> {
let request = new StorySearchQuery(this.state.currentApplication.namespace, this.state.currentApplication.name, this.state.currentLocale, 0, 1000, "Builtin Satisfaction", "builtin_satisfaction", true);
return this.rest.post(
'/analytics/satisfaction/active', request, (res: string) => BooleanResponse.fromJSON(res || {}).success);
let request = new StorySearchQuery(
this.state.currentApplication.namespace,
this.state.currentApplication.name,
this.state.currentLocale,
0,
1000,
'Builtin Satisfaction',
'builtin_satisfaction',
true
);
return this.rest.post('/analytics/satisfaction/active', request, (res: string) => BooleanResponse.fromJSON(res || {}).success);
}

createSatisfactionModule(): Observable<Boolean> {
return this.rest.post(
'/analytics/satisfaction/init', this.state.createApplicationScopedQuery(), (res: string) => BooleanResponse.fromJSON(res || {}).success);
'/analytics/satisfaction/init',
this.state.createApplicationScopedQuery(),
(res: string) => BooleanResponse.fromJSON(res || {}).success
);
}

getSatisfactionStat(): Observable<RatingReportQueryResult> {
return this.rest.post(
'/analytics/satisfaction', this.state.createApplicationScopedQuery());
return this.rest.post('/analytics/satisfaction', this.state.createApplicationScopedQuery());
}

downloadDialogsCsv(dialogReportQuery: DialogReportQuery): Observable<Blob> {
return this.rest.post('/dialogs/ratings/export',dialogReportQuery, (r) => new Blob([r], { type: 'text/csv;charset=utf-8' }));
return this.rest.post('/dialogs/ratings/export', dialogReportQuery, (r) => new Blob([r], { type: 'text/csv;charset=utf-8' }));
}

downloadDialogsWithIntentsCsv(dialogReportQuery: DialogReportQuery): Observable<Blob> {
return this.rest.post('/dialogs/ratings/intents/export',dialogReportQuery, (r) => new Blob([r], { type: 'text/csv;charset=utf-8' }));
return this.rest.post('/dialogs/ratings/intents/export', dialogReportQuery, (r) => new Blob([r], { type: 'text/csv;charset=utf-8' }));
}


}

export class BooleanResponse {
Expand Down
48 changes: 48 additions & 0 deletions bot/admin/web/src/app/analytics/dialog/dialog.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<tock-sticky-menu [offset]="50">
<div class="d-flex align-items-center">
<h1 class="flex-grow-1">Dialog</h1>

<section class="grid-actions">
<div nbPopover="Url copied">
<button
nbButton
ghost
shape="round"
nbTooltip="Copy url"
(click)="copyUrl()"
>
<nb-icon icon="share"></nb-icon>
</button>
</div>

<button
nbButton
ghost
shape="round"
nbTooltip="Go to dialogs monitoring"
(click)="jumpToDialogs(true)"
>
<nb-icon icon="wechat"></nb-icon>
</button>
</section>
</div>
</tock-sticky-menu>

<tock-no-data-found
*ngIf="accessDenied"
title="Access denied or resource deleted"
message="Your access rights do not allow you to view this resource, or the resource has been deleted."
></tock-no-data-found>

<nb-card *ngIf="dialog">
<nb-card-body class="pl-3 pr-0">
<tock-chat-ui>
<tock-chat-ui-dialog-logger
[dialog]="dialog"
[highlightedAction]="getTargetedAction()"
></tock-chat-ui-dialog-logger>
</tock-chat-ui>
</nb-card-body>
</nb-card>

<tock-scroll-top-button></tock-scroll-top-button>
7 changes: 7 additions & 0 deletions bot/admin/web/src/app/analytics/dialog/dialog.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.grid-actions {
display: grid;
grid-gap: 0.5rem;
grid-auto-flow: column;
align-items: center;
justify-content: end;
}
23 changes: 23 additions & 0 deletions bot/admin/web/src/app/analytics/dialog/dialog.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { DialogComponent } from './dialog.component';

describe('DialogComponent', () => {
let component: DialogComponent;
let fixture: ComponentFixture<DialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DialogComponent]
})
.compileComponents();

fixture = TestBed.createComponent(DialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
139 changes: 139 additions & 0 deletions bot/admin/web/src/app/analytics/dialog/dialog.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, take, takeUntil } from 'rxjs';
import { AnalyticsService } from '../analytics.service';
import { StateService } from '../../core-nlp/state.service';
import { ActionReport, DialogReport } from '../../shared/model/dialog-data';
import { ApplicationService } from '../../core-nlp/applications.service';
import { AuthService } from '../../core-nlp/auth/auth.service';
import { SettingsService } from '../../core-nlp/settings.service';
import { copyToClipboard } from '../../shared/utils';

@Component({
selector: 'tock-dialog',
templateUrl: './dialog.component.html',
styleUrl: './dialog.component.scss'
})
export class DialogComponent implements OnInit, OnDestroy {
destroy = new Subject();

dialog: DialogReport;

accessDenied: boolean = false;

targetFragment: string;

constructor(
private route: ActivatedRoute,
private analytics: AnalyticsService,
private state: StateService,
private applicationService: ApplicationService,
public auth: AuthService,
public settings: SettingsService,
private router: Router
) {}

ngOnInit(): void {
this.initialize();
}

initialize(): void {
this.route.fragment.subscribe((fragment: string) => {
if (fragment) {
this.targetFragment = fragment;
}
});

this.route.params.pipe(take(1)).subscribe((routeParams) => {
// User does not have rights to access this namespace
if (!this.state.namespaces.find((ns) => ns.namespace === routeParams.namespace)) {
this.accessDenied = true;

return;
}

// The current application does not belong to the requested namespace, so we move to the target namespace.
if (routeParams.namespace !== this.state.currentApplication.namespace) {
this.applicationService
.selectNamespace(routeParams.namespace)
.pipe(take(1))
.subscribe((_) => {
this.auth.loadUser().subscribe((_) => {
this.state.currentApplicationEmitter.pipe(take(1)).subscribe((arg) => {
this.initialize();
});
this.applicationService.resetConfiguration();
});
});

return;
}

// The current application is not the requested one, we move to the targeted application.
if (routeParams.applicationId !== this.state.currentApplication._id) {
const targetApp = this.state.applications.find((app) => app._id === routeParams.applicationId);
if (targetApp) {
this.state.currentApplicationEmitter.pipe(take(1)).subscribe((arg) => {
this.initialize();
});
this.state.changeApplicationWithName(targetApp.name);
} else {
this.accessDenied = true;
}

return;
}

this.analytics
.dialog(this.state.currentApplication._id, routeParams.dialogId)
.pipe(take(1))
.subscribe((dialog) => {
if (dialog?.actions?.length) {
this.dialog = dialog;

if (this.targetFragment) {
setTimeout(() => {
const target = document.querySelector(`#action-anchor-${this.targetFragment}`);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}
} else {
this.accessDenied = true;
}
});

// We monitor changes in the current application to redirect to the dialogs page in case of change.
this.state.currentApplicationEmitter.pipe(takeUntil(this.destroy)).subscribe((arg) => {
if (
routeParams.namespace !== this.state.currentApplication.namespace ||
routeParams.applicationId !== this.state.currentApplication._id
) {
this.jumpToDialogs();
}
});
});
}

getTargetedAction(): ActionReport {
if (!this.targetFragment || !this.dialog) return;
return this.dialog.actions.find((action) => {
return action.id === this.targetFragment;
});
}

jumpToDialogs(addAnchorRef: boolean = false): void {
const extras = addAnchorRef && this.dialog?.id ? { state: { dialogId: this.dialog.id } } : undefined;
this.router.navigateByUrl('/analytics/dialogs', extras);
}

copyUrl(): void {
copyToClipboard(window.location.href);
}

ngOnDestroy(): void {
this.destroy.next(true);
this.destroy.complete();
}
}
Loading