diff --git a/Dockerfile b/Dockerfile index 467125a34e79359212f1e71b72780e1cc346794f..b901ddc1727daf0a4fa75f3650b8d1ad089c0059 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/angular.json b/angular.json index dbbca1144b54d9cdde098ce0a2ad8a4f3f3e5654..a27caa9f07e8d00324c0516391e95ef57f260096 100644 --- a/angular.json +++ b/angular.json @@ -37,6 +37,9 @@ }, { "input": "src/global.scss" + }, + { + "input": "node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css" } ], "scripts": [] diff --git a/package-lock.json b/package-lock.json index fe6181bec9658ac5173245e8066493471c5b5181..a0043e0e5c89a63a59cb2904b2a81f45054811ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "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", diff --git a/package.json b/package.json index f27dd63afc5ae51023469d1c22f9393d83e347ec..2f427b066be6248536eeb7b97a9b426bf632ab3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", diff --git a/src/app/exercise/exercise.page.spec.ts b/src/app/exercise/exercise.page.spec.ts index b429901672b5570ade25dd710bf6486b7a4e2f7b..9bae06e0d2373b335a61436d6a4d897779befe6c 100644 --- a/src/app/exercise/exercise.page.spec.ts +++ b/src/app/exercise/exercise.page.spec.ts @@ -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(); diff --git a/src/app/semantics/semantics.module.ts b/src/app/semantics/semantics.module.ts index 2a22a273e47d0f0930b08c7e217866094712bdb1..39d55dc193538e0f36c5674cb187ca33960ed28c 100644 --- a/src/app/semantics/semantics.module.ts +++ b/src/app/semantics/semantics.module.ts @@ -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] }) diff --git a/src/app/semantics/semantics.page.html b/src/app/semantics/semantics.page.html index c5bdebfa70b263d04738c50787ccee561e81cfd0..c8c647fc679c41d3dceaa5085b75848ad50465c7 100644 --- a/src/app/semantics/semantics.page.html +++ b/src/app/semantics/semantics.page.html @@ -23,18 +23,27 @@ - + + {{ 'FIND_SIMILAR_CONTEXTS' | translate }} + - +
+ + + + + +
- {{ 'APPLY' | translate }} + {{ 'APPLY' | translate }} -
+
+ + + + + {{tok}}{{ getWhiteSpace() }} + + + +
diff --git a/src/app/semantics/semantics.page.scss b/src/app/semantics/semantics.page.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..555718a633b88fd7499537c9cd717711645cef5a 100644 --- a/src/app/semantics/semantics.page.scss +++ b/src/app/semantics/semantics.page.scss @@ -0,0 +1,3 @@ +.highlight { + color: red; +} diff --git a/src/app/semantics/semantics.page.spec.ts b/src/app/semantics/semantics.page.spec.ts index 4f5cb4a663babbfccf8e031083fac48cae798094..5c86a7ee3c435ac390a5fe0f1c876615c1c53a39 100644 --- a/src/app/semantics/semantics.page.spec.ts +++ b/src/app/semantics/semantics.page.spec.ts @@ -1,6 +1,4 @@ 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(); }); }); }); diff --git a/src/app/semantics/semantics.page.ts b/src/app/semantics/semantics.page.ts index 3e9a194c27f4a9c413df6a85dd26c8784902b82c..aa4d7e7eb42273a4f40da14750edbc58767f244d 100644 --- a/src/app/semantics/semantics.page.ts +++ b/src/app/semantics/semantics.page.ts @@ -1,4 +1,4 @@ -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 = new Set(); + 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 { + getSimilarContexts(): Promise { + return new Promise((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(); + 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 { return new Promise(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 { - const retVal: Promise = new Promise((resolve, reject) => { - if (!this.searchRegex) { - this.helperService.showToast(this.toastCtrl, this.corpusService.searchRegexMissingString).then(); - return reject(); - } + return new Promise((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(); + } + } + } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index dbccc05756708c09b578b09122ac2a4e6faf1a6e..acd20548533d86881d7a8acf3d66ee9459671644 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -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", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e2c7a78e1c4ebb8b9fbf94d34d47f3f73c7c39fd..656a5f3a28491333df6751c132f6fecee23b8797 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -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",