Commit 31ffd1b7 authored by Konstantin Schulz's avatar Konstantin Schulz
Browse files

added KWIC mode to the semantics page

parent bfeb00fa
Pipeline #10956 failed with stage
in 2 minutes and 36 seconds
......@@ -3,16 +3,15 @@ FROM node:10.19.0-stretch
RUN useradd -ms /bin/bash mc
WORKDIR /home/mc
# for testing
RUN apt update
# for testing
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN apt install -y ./google-chrome-stable_current_amd64.deb
WORKDIR /home/mc/mc_frontend
COPY package.json .
# to get the version of the local CLI package, run: npm list @angular/cli | sed 's/[^0-9.]*//g' | sed -n 2p
RUN npm i -g cordova @ionic/cli @angular/cli
RUN cordova telemetry off
RUN npm i -g @angular/cli
# this makes the analytics prompt during upcoming "npm install" disappear, so this can also run in CI
RUN ng analytics off
RUN npm install
......
......@@ -37,6 +37,9 @@
},
{
"input": "src/global.scss"
},
{
"input": "node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css"
}
],
"scripts": []
......
{
"name": "mc_frontend",
"version": "1.7.1",
"version": "1.7.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -2049,6 +2049,27 @@
}
}
},
"@angular/animations": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.0.4.tgz",
"integrity": "sha512-zTCgrIAA9FYPMbqqpQnoNltiLR58q0FMfzP2t96q/1tjyVy/Y/IaNgVQ7eL0HeQ0nG6IAzQ1HVx8Xeneg4Yj5Q=="
},
"@angular/cdk": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.0.tgz",
"integrity": "sha512-jeeznvNDpR9POuxzz8Y0zFvMynG9HCJo3ZPTqOjlOq8Lj8876+rLsHDvKEMeLdwlkdi1EweYJW1CLQzI+TwqDA==",
"requires": {
"parse5": "^5.0.0"
},
"dependencies": {
"parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"optional": true
}
}
},
"@angular/cli": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.0.4.tgz",
......@@ -2439,6 +2460,11 @@
"integrity": "sha512-a/Bqf19+YhqACxQOkpYB0HK/zjHqDrZVUyBtdiX17njuvlWgD4wvdtdxae//ZeIVHNDJS+G5Gelbe+Yzon+VGA==",
"dev": true
},
"@angular/material": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-9.2.0.tgz",
"integrity": "sha512-KKzEIVh6/m56m+Ao8p4PK0SyEr0574l3VP2swj1qPag3u+FYgemmXCGTaChrKdDsez+zeTCPXImBGXzE6NQ80Q=="
},
"@angular/platform-browser": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.0.4.tgz",
......
{
"name": "mc_frontend",
"version": "1.7.7",
"version": "1.7.9",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
......@@ -16,9 +16,12 @@
"private": true,
"dependencies": {
"@angular-builders/custom-webpack": "^9.0.0",
"@angular/animations": "^9.0.4",
"@angular/cdk": "^9.2.0",
"@angular/common": "^9.0.4",
"@angular/core": "^9.0.4",
"@angular/forms": "^9.0.4",
"@angular/material": "^9.2.0",
"@angular/platform-browser": "^9.0.4",
"@angular/platform-browser-dynamic": "^9.0.4",
"@angular/router": "^9.0.4",
......
......@@ -9,7 +9,6 @@ import {APP_BASE_HREF} from '@angular/common';
import {of} from 'rxjs';
import {AnnisResponse} from '../models/annisResponse';
import {ExerciseType, MoodleExerciseType} from '../models/enum';
import MockMC from '../models/mockMC';
import Spy = jasmine.Spy;
import configMC from '../../configMC';
......@@ -39,9 +38,8 @@ describe('ExercisePage', () => {
.compileComponents().then();
fixture = TestBed.createComponent(ExercisePage);
exercisePage = fixture.componentInstance;
exercisePage.helperService.applicationState.next(exercisePage.helperService.deepCopy(MockMC.applicationState));
h5pSpy = spyOn(exercisePage.exerciseService, 'initH5P').and.returnValue(Promise.resolve());
checkSpy = spyOn(exercisePage.corpusService, 'checkAnnisResponse').and.returnValue(Promise.resolve());
checkSpy = spyOn(exercisePage.corpusService, 'checkAnnisResponse').and.callFake(() => Promise.reject());
getRequestSpy = spyOn(exercisePage.helperService, 'makeGetRequest').and.returnValue(Promise.resolve(
new AnnisResponse({exercise_type: MoodleExerciseType.cloze.toString()})));
fixture.detectChanges();
......
......@@ -8,6 +8,7 @@ import {SemanticsPageRoutingModule} from './semantics-routing.module';
import {SemanticsPage} from './semantics.page';
import {TranslateModule} from '@ngx-translate/core';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
@NgModule({
imports: [
......@@ -16,6 +17,7 @@ import {TranslateModule} from '@ngx-translate/core';
IonicModule,
SemanticsPageRoutingModule,
TranslateModule.forChild(),
MatSlideToggleModule,
],
declarations: [SemanticsPage]
})
......
......@@ -23,18 +23,27 @@
<ion-grid>
<ion-row>
<ion-col>
<label>{{ 'SEARCH' | translate }}
<input type="text" [(ngModel)]="searchRegex" name="searchRegex">
</label>
<mat-slide-toggle [(ngModel)]="isKWICview" color="primary" name="isKWICview">
{{ 'FIND_SIMILAR_CONTEXTS' | translate }}
</mat-slide-toggle>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<label>{{ 'MINIMUM_WORD_FREQUENCY_COUNT' | translate }}
<input type="number" min="1" [(ngModel)]="minCount" name="minCount">
<label>{{ 'SEARCH' | translate }}
<input type="text" [(ngModel)]="searchRegex" name="searchRegex">
</label>
</ion-col>
</ion-row>
<div *ngIf="!isKWICview">
<ion-row>
<ion-col>
<label>{{ 'MINIMUM_WORD_FREQUENCY_COUNT' | translate }}
<input type="number" min="1" [(ngModel)]="minCount" name="minCount">
</label>
</ion-col>
</ion-row>
</div>
<ion-row>
<ion-col>
<label>{{ 'NEAREST_NEIGHBORS_COUNT' | translate }}
......@@ -51,13 +60,23 @@
</ion-row>
<ion-row>
<ion-col>
<ion-button (click)="updateVectorNetwork()">{{ 'APPLY' | translate }}</ion-button>
<ion-button (click)="updateView()">{{ 'APPLY' | translate }}</ion-button>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<ion-spinner *ngIf="helperService.openRequests.length"></ion-spinner>
<div id="{{svgElementSelector.slice(1)}}"></div>
<div *ngIf="!isKWICview; else contexts" id="{{svgElementSelector.slice(1)}}"></div>
<ng-template #contexts>
<ion-grid>
<ion-row *ngFor="let context of similarContexts">
<ion-col style="text-align: left">
<span *ngFor="let tok of context"><span
[class.highlight]="highlightSet.has(tok)">{{tok}}</span>{{ getWhiteSpace() }}</span>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
</ion-col>
</ion-row>
<ion-row>
......
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {IonicModule} from '@ionic/angular';
import {SemanticsPage} from './semantics.page';
import {RouterModule} from '@angular/router';
import {HttpClientModule, HttpErrorResponse} from '@angular/common/http';
......@@ -10,6 +8,8 @@ import {FormsModule} from '@angular/forms';
import {of} from 'rxjs';
import {APP_BASE_HREF} from '@angular/common';
import Spy = jasmine.Spy;
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
describe('SemanticsPage', () => {
let semanticsPage: SemanticsPage;
......@@ -22,12 +22,14 @@ describe('SemanticsPage', () => {
FormsModule,
HttpClientModule,
IonicStorageModule.forRoot(),
MatSlideToggleModule,
RouterModule.forRoot([]),
TranslateTestingModule,
],
providers: [
{provide: APP_BASE_HREF, useValue: '/'},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents().then();
fixture = TestBed.createComponent(SemanticsPage);
semanticsPage = fixture.componentInstance;
......@@ -36,21 +38,42 @@ describe('SemanticsPage', () => {
it('should create', () => {
expect(semanticsPage).toBeTruthy();
expect(semanticsPage.getWhiteSpace()).toBe(' ');
});
it('should get similar contexts', (done) => {
const requestSpy: Spy = spyOn(semanticsPage.helperService, 'makePostRequest').and.callFake(() => Promise.reject());
semanticsPage.getSimilarContexts().then(() => {
}, () => {
expect(semanticsPage.similarContexts.length).toBe(0);
requestSpy.and.returnValue(Promise.resolve([['a']]));
semanticsPage.highlightRegex = 'a';
semanticsPage.getSimilarContexts().then(() => {
expect(semanticsPage.similarContexts.length).toBe(1);
expect(semanticsPage.highlightSet.size).toBe(1);
done();
});
});
});
it('should be initialized', (done) => {
spyOn(semanticsPage, 'updateVectorNetwork');
const updateSpy: Spy = spyOn(semanticsPage, 'updateVectorNetwork').and.returnValue(Promise.resolve());
const contextSpy: Spy = spyOn(semanticsPage, 'getSimilarContexts').and.returnValue(Promise.resolve());
semanticsPage.activatedRoute.queryParams = of({minCount: '0'});
semanticsPage.ngOnInit().then(() => {
semanticsPage.ngAfterViewInit().then(() => {
expect(semanticsPage.minCount).toBe(0);
semanticsPage.activatedRoute.queryParams = of({
searchRegex: 'a',
highlightRegex: 'b',
nearestNeighborCount: '0'
});
semanticsPage.ngOnInit().then(() => {
expect(semanticsPage.nearestNeighborCount).toBe(0);
done();
semanticsPage.ngAfterViewInit().then(() => {
expect(updateSpy).toHaveBeenCalledTimes(1);
semanticsPage.isKWICview = true;
semanticsPage.ngAfterViewInit().then(() => {
expect(contextSpy).toHaveBeenCalledTimes(1);
done();
});
});
});
});
......@@ -58,16 +81,14 @@ describe('SemanticsPage', () => {
it('should update the vector network', (done) => {
const requestSpy: Spy = spyOn(semanticsPage.helperService, 'makeGetRequest').and.returnValue(Promise.resolve('a'));
const toastSpy: Spy = spyOn(semanticsPage.helperService, 'showToast').and.returnValue(Promise.resolve());
semanticsPage.searchRegex = 'a';
semanticsPage.updateVectorNetwork().then(() => {
expect(toastSpy).toHaveBeenCalledTimes(1);
semanticsPage.searchRegex = 'a';
expect(semanticsPage.kwicGraphs).toBeTruthy();
requestSpy.and.callFake(() => Promise.reject(new HttpErrorResponse({status: 422})));
semanticsPage.updateVectorNetwork().then(() => {
expect(semanticsPage.kwicGraphs).toBeTruthy();
requestSpy.and.callFake(() => Promise.reject(new HttpErrorResponse({status: 422})));
semanticsPage.updateVectorNetwork().then(() => {
expect(toastSpy).toHaveBeenCalledTimes(2);
done();
});
}, () => {
expect(toastSpy).toHaveBeenCalledTimes(1);
done();
});
});
});
......
import {Component, OnInit} from '@angular/core';
import {AfterViewInit, Component} from '@angular/core';
import {NavController, ToastController} from '@ionic/angular';
import {HelperService} from '../helper.service';
import configMC from '../../configMC';
......@@ -11,8 +11,10 @@ import {CorpusService} from '../corpus.service';
templateUrl: './semantics.page.html',
styleUrls: ['./semantics.page.scss'],
})
export class SemanticsPage implements OnInit {
export class SemanticsPage implements AfterViewInit {
public highlightRegex = '';
public highlightSet: Set<string> = new Set<string>();
public isKWICview = false;
public kwicGraphs: string;
public metadata: string[] = ('XII panegyrici Latini\n' +
'Baehrens, Emil\n' +
......@@ -23,6 +25,7 @@ export class SemanticsPage implements OnInit {
public minCount = 1;
public nearestNeighborCount = 1;
public searchRegex = '';
public similarContexts: string[][] = [];
public svgElementSelector = '#svg';
constructor(public navCtrl: NavController,
......@@ -33,15 +36,45 @@ export class SemanticsPage implements OnInit {
public corpusService: CorpusService) {
}
ngOnInit(): Promise<void> {
getSimilarContexts(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const formData: FormData = new FormData();
formData.append('search_regex', this.searchRegex);
formData.append('nearest_neighbor_count', (Math.max(Math.round(this.nearestNeighborCount), 1)).toString());
const semanticsUrl: string = configMC.backendBaseUrl + configMC.backendApiVectorNetworkPath;
this.similarContexts = [];
this.helperService.makePostRequest(this.http, this.toastCtrl, semanticsUrl, formData).then((contexts: string[][]) => {
this.similarContexts = contexts;
this.highlightSet = new Set<string>();
if (this.highlightRegex) {
const regex: RegExp = new RegExp(this.highlightRegex);
this.similarContexts.forEach((context: string[]) => {
context.forEach((tok: string) => {
if (regex.test(tok)) {
this.highlightSet.add(tok);
}
});
});
}
return resolve();
}, () => {
return reject();
});
});
}
getWhiteSpace(): string {
return ' ';
}
ngAfterViewInit(): Promise<void> {
return new Promise<void>(resolve => {
this.activatedRoute.queryParams.subscribe((params: any) => {
if (Object.keys(params).length) {
Object.keys(params).forEach((key: string) => {
this[key] = typeof this[key] === 'number' ? +params[key] : params[key];
});
// dirty hack to get the loading spinner displayed correctly
setTimeout(this.updateVectorNetwork.bind(this), 500);
this.updateView();
}
return resolve();
});
......@@ -49,19 +82,15 @@ export class SemanticsPage implements OnInit {
}
updateVectorNetwork(): Promise<void> {
const retVal: Promise<void> = new Promise<void>((resolve, reject) => {
if (!this.searchRegex) {
this.helperService.showToast(this.toastCtrl, this.corpusService.searchRegexMissingString).then();
return reject();
}
return new Promise<void>((resolve, reject) => {
let params: HttpParams = new HttpParams().set('search_regex', this.searchRegex);
params = params.set('min_count', (Math.max(Math.round(this.minCount), 1)).toString());
params = params.set('highlight_regex', this.highlightRegex);
params = params.set('nearest_neighbor_count', (Math.max(Math.round(this.nearestNeighborCount), 1)).toString());
const kwicUrl: string = configMC.backendBaseUrl + configMC.backendApiVectorNetworkPath;
const semanticsUrl: string = configMC.backendBaseUrl + configMC.backendApiVectorNetworkPath;
const svgElement: SVGElement = document.querySelector(this.svgElementSelector);
svgElement.innerHTML = '';
this.helperService.makeGetRequest(this.http, this.toastCtrl, kwicUrl, params).then((svgString: string) => {
this.helperService.makeGetRequest(this.http, this.toastCtrl, semanticsUrl, params).then((svgString: string) => {
this.kwicGraphs = svgString;
svgElement.innerHTML = this.kwicGraphs;
return resolve();
......@@ -72,9 +101,17 @@ export class SemanticsPage implements OnInit {
return reject();
});
});
// dirty hack to prevent unhandled promise rejection in click events
return retVal.catch(() => {
});
}
updateView(): void {
if (!this.searchRegex) {
this.helperService.showToast(this.toastCtrl, this.corpusService.searchRegexMissingString).then();
} else {
if (this.isKWICview) {
this.getSimilarContexts().then();
} else {
this.updateVectorNetwork().then();
}
}
}
}
......@@ -203,6 +203,7 @@
"FILE_TYPE_DOCX": "DOCX",
"FILE_TYPE_PDF": "PDF",
"FILE_TYPE_XML": "XML",
"FIND_SIMILAR_CONTEXTS": "Ähnliche Kontexte finden",
"GENERATE_FILE_DOCX": "Word-Datei generieren",
"GENERATE_FILE_PDF": "PDF generieren",
"GIVEN": "Gegeben",
......
......@@ -203,6 +203,7 @@
"FILE_TYPE_DOCX": "DOCX",
"FILE_TYPE_PDF": "PDF",
"FILE_TYPE_XML": "XML",
"FIND_SIMILAR_CONTEXTS": "Find similar contexts",
"GENERATE_FILE_DOCX": "Generate Word file",
"GENERATE_FILE_PDF": "Generate PDF",
"GIVEN": "Given",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment