Initial (dirty) commit.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a3d1ac2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+<img src="https://nlpcraft.org/images/nlpcraft_logo_black.gif" height="80px">
+<br>
+<a target=_ href="https://gitter.im/nlpcraftorg/community"><img alt="Gitter" src="https://badges.gitter.im/nlpcraftorg/community.svg"></a>
+
+# Web-based UI for NLPCraft
+
+## Live Version
+
+[https://nlpcrafters.github.io/nlpcraft-ui/](https://nlpcrafters.github.io/nlpcraft-ui/)
+
+## Software Requirements
+
+- [NodeJS 8+](https://nodejs.org)
+- [Yarn 1.19+](https://yarnpkg.com)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Angular CLI](https://cli.angular.io/)
+
+
+## Prepare (Run only once)
+
+ - Run `ng set --global packageManager=yarn` - configures Angular CLI to use Yarn
+ - Run `yarn` - initializes dependencies
+
+## Build
+
+ - Run `yarn build`
+ - See `dist/web-app` folder for build results
+
+
+## Development mode
+
+ - Run `yarn start`
+ - Open browser at [http://localhost:4200](http://localhost:4200)
diff --git a/angular.json b/angular.json
new file mode 100644
index 0000000..3e21895
--- /dev/null
+++ b/angular.json
@@ -0,0 +1,136 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "web-app": {
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "prefix": "app",
+ "schematics": {},
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/web-app",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.app.json",
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": [],
+ "es5BrowserSupport": true
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "extractCss": true,
+ "namedChunks": false,
+ "aot": true,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true,
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "2mb",
+ "maximumError": "5mb"
+ }
+ ]
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "web-app:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "web-app:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "web-app:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.spec.json",
+ "karmaConfig": "src/karma.conf.js",
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": [],
+ "assets": [
+ "src/favicon.ico",
+ "src/assets"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "src/tsconfig.app.json",
+ "src/tsconfig.spec.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ },
+ "web-app-e2e": {
+ "root": "e2e/",
+ "projectType": "application",
+ "prefix": "",
+ "architect": {
+ "e2e": {
+ "builder": "@angular-devkit/build-angular:protractor",
+ "options": {
+ "protractorConfig": "e2e/protractor.conf.js",
+ "devServerTarget": "web-app:serve"
+ },
+ "configurations": {
+ "production": {
+ "devServerTarget": "web-app:serve:production"
+ }
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": "e2e/tsconfig.e2e.json",
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "web-app"
+}
diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js
new file mode 100644
index 0000000..8a99d37
--- /dev/null
+++ b/e2e/protractor.conf.js
@@ -0,0 +1,29 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const {SpecReporter} = require('jasmine-spec-reporter');
+
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './src/**/*.e2e-spec.ts'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function () {
+ }
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: require('path').join(__dirname, './tsconfig.e2e.json')
+ });
+ jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
+ }
+};
diff --git a/e2e/src/app.e2e-spec.ts b/e2e/src/app.e2e-spec.ts
new file mode 100644
index 0000000..4a9bfff
--- /dev/null
+++ b/e2e/src/app.e2e-spec.ts
@@ -0,0 +1,23 @@
+import {browser, logging} from 'protractor'
+import {AppPage} from './app.po'
+
+describe('workspace-project App', () => {
+ let page: AppPage
+
+ beforeEach(() => {
+ page = new AppPage()
+ })
+
+ it('should display welcome message', () => {
+ page.navigateTo()
+ expect(page.getTitleText()).toEqual('Welcome to web-app!')
+ })
+
+ afterEach(async () => {
+ // Assert that there are no errors emitted from the browser
+ const logs = await browser.manage().logs().get(logging.Type.BROWSER)
+ expect(logs).not.toContain(jasmine.objectContaining({
+ level: logging.Level.SEVERE
+ } as logging.Entry))
+ })
+})
diff --git a/e2e/src/app.po.ts b/e2e/src/app.po.ts
new file mode 100644
index 0000000..63e5f25
--- /dev/null
+++ b/e2e/src/app.po.ts
@@ -0,0 +1,11 @@
+import {browser, by, element} from 'protractor'
+
+export class AppPage {
+ navigateTo() {
+ return browser.get(browser.baseUrl) as Promise<any>
+ }
+
+ getTitleText() {
+ return element(by.css('app-root h1')).getText() as Promise<string>
+ }
+}
diff --git a/e2e/tsconfig.e2e.json b/e2e/tsconfig.e2e.json
new file mode 100644
index 0000000..99823ac
--- /dev/null
+++ b/e2e/tsconfig.e2e.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "jasminewd2",
+ "node"
+ ]
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..907b769
--- /dev/null
+++ b/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "nlpcraft-ui",
+ "version": "0.0.0",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng lint && ng build --prod=true",
+ "lint": "ng lint"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "~7.2.0",
+ "@angular/common": "~7.2.0",
+ "@angular/compiler": "~7.2.0",
+ "@angular/core": "~7.2.0",
+ "@angular/forms": "~7.2.0",
+ "@angular/platform-browser": "~7.2.0",
+ "@angular/platform-browser-dynamic": "~7.2.0",
+ "@angular/router": "~7.2.0",
+ "@fortawesome/angular-fontawesome": "^0.3.0",
+ "@fortawesome/fontawesome-svg-core": "^1.2.18",
+ "@fortawesome/free-regular-svg-icons": "^5.8.2",
+ "@fortawesome/free-solid-svg-icons": "^5.8.2",
+ "@ng-bootstrap/ng-bootstrap": "^4.1.3",
+ "bootstrap": "^4.3.1",
+ "core-js": "^2.5.4",
+ "rxjs": "~6.3.3",
+ "tslib": "^1.9.0",
+ "zone.js": "~0.8.26"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "~0.13.0",
+ "@angular/cli": "~7.3.9",
+ "@angular/compiler-cli": "~7.2.0",
+ "@angular/language-service": "~7.2.0",
+ "@types/jasmine": "~2.8.8",
+ "@types/jasminewd2": "~2.0.3",
+ "@types/node": "~8.9.4",
+ "codelyzer": "~4.5.0",
+ "jasmine-core": "~2.99.1",
+ "jasmine-spec-reporter": "~4.2.1",
+ "karma": "~4.0.0",
+ "karma-chrome-launcher": "~2.2.0",
+ "karma-coverage-istanbul-reporter": "~2.0.1",
+ "karma-jasmine": "~1.1.2",
+ "karma-jasmine-html-reporter": "^0.2.2",
+ "protractor": "~5.4.0",
+ "ts-node": "~7.0.0",
+ "tslint": "~5.11.0",
+ "typescript": "~3.2.2"
+ }
+}
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
new file mode 100644
index 0000000..8f1f6b5
--- /dev/null
+++ b/src/app/app-routing.module.ts
@@ -0,0 +1,30 @@
+import {NgModule} from '@angular/core'
+import {RouterModule, Routes} from '@angular/router'
+import {RouterSessionGuard} from './services/router/router-session.guard'
+import {LoginComponent} from './ui/login/login.component'
+import {MainComponent} from './ui/main/main.component'
+
+const routes: Routes = [
+ {
+ path: 'login',
+ component: LoginComponent
+ },
+ {
+ path: 'main',
+ component: MainComponent,
+ canActivate: [RouterSessionGuard]
+ },
+ {
+ path: '',
+ redirectTo: '/main',
+ pathMatch: 'full'
+ }
+]
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes, {useHash: true})],
+ exports: [RouterModule]
+})
+export class AppRoutingModule {
+ // No-op.
+}
diff --git a/src/app/app.component.css b/src/app/app.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/app.component.css
diff --git a/src/app/app.component.html b/src/app/app.component.html
new file mode 100644
index 0000000..517b777
--- /dev/null
+++ b/src/app/app.component.html
@@ -0,0 +1,6 @@
+<div class="d-flex flex-column">
+ <app-navbar></app-navbar>
+
+ <router-outlet></router-outlet>
+</div>
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
new file mode 100644
index 0000000..5706969
--- /dev/null
+++ b/src/app/app.component.ts
@@ -0,0 +1,42 @@
+import {Component, OnDestroy, OnInit} from '@angular/core'
+import {Subscription} from 'rxjs'
+import {RouterService} from './services/router/router.service'
+import {SessionService} from './services/session/session.service'
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.css']
+})
+export class AppComponent implements OnInit, OnDestroy {
+ private _sessionSub: Subscription
+
+ constructor(
+ private _router: RouterService,
+ private _session: SessionService
+ ) {
+ // No-op.
+ }
+
+ async ngOnInit() {
+ try {
+ await this._session.ping()
+ } catch (e) {
+ // Ignore.
+ }
+
+ this._sessionSub = this._session.sessionChanges.subscribe(ses => {
+ if (ses) {
+ this._router.goToMain()
+ } else {
+ this._router.goToLogin()
+ }
+ })
+ }
+
+ ngOnDestroy(): void {
+ if (this._sessionSub) {
+ this._sessionSub.unsubscribe()
+ }
+ }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
new file mode 100644
index 0000000..ebefde2
--- /dev/null
+++ b/src/app/app.module.ts
@@ -0,0 +1,73 @@
+import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'
+import {NgModule} from '@angular/core'
+import {FormsModule} from '@angular/forms'
+import {BrowserModule} from '@angular/platform-browser'
+import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'
+import {library} from '@fortawesome/fontawesome-svg-core'
+import {faCommentSlash, faSignOutAlt, faSpinner, faTrashAlt} from '@fortawesome/free-solid-svg-icons'
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap'
+import {AppRoutingModule} from './app-routing.module'
+import {AppComponent} from './app.component'
+import {NlpService} from './services/nlp/nlp.service'
+import {RestErrorInterceptor} from './services/rest/rest-error.interceptor'
+import {RestUrlInterceptor} from './services/rest/rest-url.interceptor'
+import {RestUrlService} from './services/rest/rest-url.service'
+import {RouterSessionGuard} from './services/router/router-session.guard'
+import {RouterService} from './services/router/router.service'
+import {SessionService} from './services/session/session.service'
+import {LoginComponent} from './ui/login/login.component'
+import {ChatComponent} from './ui/main/chat/chat.component'
+import {DetailsComponent} from './ui/main/details/details.component'
+import {MainComponent} from './ui/main/main.component'
+import {NavbarComponent} from './ui/navbar/navbar.component'
+import {ErrorComponent} from './ui/utils/error.component'
+import {LoadSpinnerComponent} from './ui/utils/load-spinner.component'
+
+@NgModule({
+ declarations: [
+ NavbarComponent,
+ AppComponent,
+ LoginComponent,
+ MainComponent,
+ ErrorComponent,
+ LoadSpinnerComponent,
+ ChatComponent,
+ DetailsComponent
+ ],
+ imports: [
+ BrowserModule,
+ HttpClientModule,
+ NgbModule,
+ FontAwesomeModule,
+ AppRoutingModule,
+ FormsModule
+ ],
+ providers: [
+ RouterService,
+ SessionService,
+ RestUrlService,
+ NlpService,
+ RouterSessionGuard,
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: RestUrlInterceptor,
+ multi: true
+ },
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: RestErrorInterceptor,
+ multi: true
+ }
+ ],
+ bootstrap: [
+ AppComponent
+ ]
+})
+export class AppModule {
+ constructor() {
+ library.add(faSpinner)
+ library.add(faSignOutAlt)
+ library.add(faTrashAlt)
+ library.add(faCommentSlash)
+ }
+}
diff --git a/src/app/services/nlp/nlp.model.ts b/src/app/services/nlp/nlp.model.ts
new file mode 100644
index 0000000..3b6d15c
--- /dev/null
+++ b/src/app/services/nlp/nlp.model.ts
@@ -0,0 +1,91 @@
+export interface NlpAllProbesResponse {
+ readonly probes: NlpProbe[]
+}
+
+export interface NlpProbe {
+ readonly probeToken: string
+ readonly probeId: string
+ readonly probeGuid: string
+ readonly probeApiVersion: string
+ readonly probeApiDate: string
+ readonly osVersion: string
+ readonly osName: string
+ readonly osArch: string
+ readonly startTstamp: number
+ readonly tmzId: string
+ readonly tmzAbbr: string
+ readonly tmzName: string
+ readonly userName: string
+ readonly javaVersion: string
+ readonly javaVendor: string
+ readonly hostName: string
+ readonly hostAddr: string
+ readonly macAddr: string
+ readonly models: NlpModel[]
+}
+
+export interface NlpModel {
+ readonly id: string
+ readonly name: string
+ readonly version: string
+ readonly enabledTokens: string[]
+}
+
+export interface NlpAskResponse {
+ readonly status: string
+ readonly srvReqId: string
+}
+
+export interface NlpCheckResponse {
+ readonly status: string
+ readonly states: NlpQueryState[]
+}
+
+export interface NlpQueryState {
+ readonly srvReqId: string
+ readonly txt: string
+ readonly userId: number
+ readonly mdlId: string
+ readonly status: string
+ readonly error: string
+ readonly errorCode: number
+ readonly createTstamp: number
+ readonly updateTstamp: number
+ readonly probeId: string
+ readonly resType: string
+ readonly resBody: any
+ readonly logHolder: NlpLogHolder
+}
+
+export interface NlpLogHolder {
+ readonly queryContext: NlpQueryContext
+ readonly intents: NlpIntent[]
+}
+
+export interface NlpIntent {
+ readonly id: string
+ readonly exactMatch: boolean
+ readonly tokensGroups: Map<string, NlpIntentTokenGroup[]>
+}
+
+export interface NlpIntentTokenGroup {
+ readonly token: NlpIntentToken
+ readonly used: boolean
+ readonly conversation: boolean
+}
+
+export interface NlpIntentToken {
+ readonly id: string,
+ readonly groups: string[]
+ readonly metadata: Map<string, any>
+}
+
+export interface NlpQueryContext {
+ readonly variants: NlpVariant[][]
+}
+
+export interface NlpVariant {
+ readonly id: string
+ readonly groups: string[]
+ readonly metadata: Map<string, any>
+}
diff --git a/src/app/services/nlp/nlp.service.ts b/src/app/services/nlp/nlp.service.ts
new file mode 100644
index 0000000..931b2e3
--- /dev/null
+++ b/src/app/services/nlp/nlp.service.ts
@@ -0,0 +1,51 @@
+import {HttpClient} from '@angular/common/http'
+import {Injectable} from '@angular/core'
+import {Observable} from 'rxjs'
+import {SessionService} from '../session/session.service'
+import {NlpAllProbesResponse, NlpAskResponse, NlpCheckResponse} from './nlp.model'
+
+@Injectable()
+export class NlpService {
+ constructor(
+ private _http: HttpClient,
+ private _sessions: SessionService
+ ) {
+ // No-op.
+ }
+
+ check(): Observable<NlpCheckResponse> {
+ return this._http.post<NlpCheckResponse>('/check', {
+ acsTok: this._sessions.get().token
+ })
+ }
+
+ cancel(requestIds: string[]): Observable<any> {
+ return this._http.post('/cancel', {
+ acsTok: this._sessions.get().token,
+ srvReqIds: requestIds
+ })
+ }
+
+ ask(query: string, modelId: string): Observable<NlpAskResponse> {
+ return this._http.post<NlpAskResponse>('/ask', {
+ acsTok: this._sessions.get().token,
+ txt: query,
+ mdlId: modelId,
+ enableLog: true
+ })
+ }
+
+ clearConversation(modelId: string): Observable<any> {
+ return this._http.post<NlpAskResponse>('/clear/conversation', {
+ acsTok: this._sessions.get().token,
+ mdlId: modelId
+ })
+ }
+
+ allProbes(): Observable<NlpAllProbesResponse> {
+ return this._http.post<NlpAllProbesResponse>('/probe/all', {
+ acsTok: this._sessions.get().token
+ })
+ }
+
+}
diff --git a/src/app/services/rest/rest-error.interceptor.ts b/src/app/services/rest/rest-error.interceptor.ts
new file mode 100644
index 0000000..09a8a76
--- /dev/null
+++ b/src/app/services/rest/rest-error.interceptor.ts
@@ -0,0 +1,32 @@
+import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'
+import {Injectable} from '@angular/core'
+import {Observable, throwError} from 'rxjs'
+import {catchError} from 'rxjs/operators'
+import {SessionService} from '../session/session.service'
+
+@Injectable()
+export class RestErrorInterceptor implements HttpInterceptor {
+ constructor(
+ private _sessions: SessionService
+ ) {
+ // No-op.
+ }
+
+ intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ return next.handle(req)
+ .pipe(
+ catchError(err => {
+ this.check(err)
+
+ return throwError(err)
+ })
+ )
+ }
+
+ private check(err: any): void {
+ if (err.status === 401) {
+ // Clear session whenever we intercept 401 (means that session got expired).
+ this._sessions.clear()
+ }
+ }
+}
diff --git a/src/app/services/rest/rest-url.interceptor.ts b/src/app/services/rest/rest-url.interceptor.ts
new file mode 100644
index 0000000..e69a9d0
--- /dev/null
+++ b/src/app/services/rest/rest-url.interceptor.ts
@@ -0,0 +1,24 @@
+import {HttpEvent} from '@angular/common/http'
+import {HttpRequest} from '@angular/common/http'
+import {HttpHandler} from '@angular/common/http'
+import {HttpInterceptor} from '@angular/common/http'
+import {Injectable} from '@angular/core'
+import {Observable} from 'rxjs'
+import {RestUrlService} from './rest-url.service'
+
+@Injectable()
+export class RestUrlInterceptor implements HttpInterceptor {
+ constructor(
+ private _rest: RestUrlService
+ ) {
+ // No-op.
+ }
+
+ intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ const nextReq = req.clone({
+ url: this._rest.url(req.url)
+ })
+
+ return next.handle(nextReq)
+ }
+}
diff --git a/src/app/services/rest/rest-url.service.ts b/src/app/services/rest/rest-url.service.ts
new file mode 100644
index 0000000..c7aa0d4
--- /dev/null
+++ b/src/app/services/rest/rest-url.service.ts
@@ -0,0 +1,34 @@
+import {Injectable} from '@angular/core'
+
+@Injectable()
+export class RestUrlService {
+ private static readonly API_ENDPOINT_KEY = 'api_endpoint'
+
+ private _apiEndpoint: string
+
+ constructor() {
+ this._apiEndpoint = localStorage.getItem(RestUrlService.API_ENDPOINT_KEY)
+ }
+
+ get apiEndpoint(): string {
+ return this._apiEndpoint
+ }
+
+ set apiEndpoint(value: string) {
+ if (value && value.endsWith('/')) {
+ value = value.substring(0, value.length - 1)
+ }
+
+ this._apiEndpoint = value
+
+ if (value) {
+ localStorage.setItem(RestUrlService.API_ENDPOINT_KEY, value)
+ } else {
+ localStorage.removeItem(RestUrlService.API_ENDPOINT_KEY)
+ }
+ }
+
+ url(path: string): string {
+ return this._apiEndpoint + '/api/v1' + path
+ }
+}
diff --git a/src/app/services/router/router-session.guard.ts b/src/app/services/router/router-session.guard.ts
new file mode 100644
index 0000000..da36c4a
--- /dev/null
+++ b/src/app/services/router/router-session.guard.ts
@@ -0,0 +1,16 @@
+import {Injectable} from '@angular/core'
+import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot} from '@angular/router'
+import {SessionService} from '../session/session.service'
+
+@Injectable()
+export class RouterSessionGuard implements CanActivate {
+ constructor(
+ private _sessions: SessionService
+ ) {
+ // No-op.
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ return this._sessions.hasSession()
+ }
+}
diff --git a/src/app/services/router/router.service.ts b/src/app/services/router/router.service.ts
new file mode 100644
index 0000000..3dab59a
--- /dev/null
+++ b/src/app/services/router/router.service.ts
@@ -0,0 +1,19 @@
+import {Injectable} from '@angular/core'
+import {Router} from '@angular/router'
+
+@Injectable()
+export class RouterService {
+ constructor(
+ private _router: Router
+ ) {
+ // No-op.
+ }
+
+ goToLogin(): void {
+ this._router.navigate(['login'])
+ }
+
+ goToMain(): void {
+ this._router.navigate(['main'])
+ }
+}
diff --git a/src/app/services/session/session.model.ts b/src/app/services/session/session.model.ts
new file mode 100644
index 0000000..9698bfe
--- /dev/null
+++ b/src/app/services/session/session.model.ts
@@ -0,0 +1,12 @@
+export class Session {
+ constructor(
+ readonly token: string
+ ) {
+ // No-op.
+ }
+}
+
+export interface LoginResponseDto {
+ readonly status: string
+ readonly acsTok: string
+}
diff --git a/src/app/services/session/session.service.ts b/src/app/services/session/session.service.ts
new file mode 100644
index 0000000..4f67e66
--- /dev/null
+++ b/src/app/services/session/session.service.ts
@@ -0,0 +1,92 @@
+import {HttpClient} from '@angular/common/http'
+import {Injectable} from '@angular/core'
+import {BehaviorSubject, Observable} from 'rxjs'
+import {map, tap} from 'rxjs/operators'
+import {LoginResponseDto, Session} from './session.model'
+
+@Injectable()
+export class SessionService {
+ private static readonly STORE_TOKEN_KEY = 'tok'
+
+ constructor(
+ private _http: HttpClient
+ ) {
+ // No-op.
+ }
+
+ private _sessionChanges: BehaviorSubject<Session> = new BehaviorSubject(null)
+
+ get sessionChanges(): Observable<Session> {
+ return this._sessionChanges.asObservable()
+ }
+
+ get(): Session {
+ return this._sessionChanges.getValue()
+ }
+
+ hasSession(): boolean {
+ return !!this.get()
+ }
+
+ login(mail: string, pwd: string): Observable<Session> {
+ return this._http.post<LoginResponseDto>('/signin', {
+ email: mail,
+ passwd: pwd
+ }).pipe(
+ map(dto => new Session(dto.acsTok)),
+ tap(session => {
+ this.initSession(session)
+ })
+ )
+ }
+
+ ping(): Promise<Session> {
+ const tok = sessionStorage.getItem(SessionService.STORE_TOKEN_KEY)
+
+ if (tok) {
+ return this._http
+ .post('/check', {
+ acsTok: tok,
+ maxRows: 1
+ })
+ .pipe(
+ map(() => new Session(tok)),
+ tap(session => {
+ if (!this.hasSession() || this.get().token !== tok) {
+ this.initSession(session)
+ }
+ })
+ )
+ .toPromise()
+ } else {
+ return Promise.resolve(null)
+ }
+ }
+
+ logout(): Promise<any> {
+ if (this.hasSession()) {
+ try {
+ // Use Promise here to force request execution before we clear all the local data.
+ return this._http.post('/sigout', {
+ acsTok: this.get().token
+ }).toPromise()
+ } finally {
+ this.clear()
+ }
+ }
+
+ return Promise.resolve()
+ }
+
+ clear() {
+ sessionStorage.removeItem(SessionService.STORE_TOKEN_KEY)
+
+ this._sessionChanges.next(null)
+ }
+
+ private initSession(session: Session) {
+ sessionStorage.setItem(SessionService.STORE_TOKEN_KEY, session.token)
+
+ this._sessionChanges.next(session)
+ }
+}
diff --git a/src/app/ui/login/login.component.css b/src/app/ui/login/login.component.css
new file mode 100644
index 0000000..a4e17ee
--- /dev/null
+++ b/src/app/ui/login/login.component.css
@@ -0,0 +1,22 @@
+
+
+.login-section {
+ width: 350px;
+ text-align: center;
+ position: relative;
+ min-height: 350px;
+}
+
+.login-section .login-section-bg {
+ border-radius: 10px;
+ background-color: #eee;
+ position: absolute;
+ padding: 20px;
+ margin: 20px 0;
+ width: 100%;
+}
+
+.login-section .login-section-bg .form-control {
+ border: 1px solid #ddd;
+ padding: 0.5rem .75rem;
+}
diff --git a/src/app/ui/login/login.component.html b/src/app/ui/login/login.component.html
new file mode 100644
index 0000000..558295e
--- /dev/null
+++ b/src/app/ui/login/login.component.html
@@ -0,0 +1,65 @@
+<div class="main-content d-flex justify-content-center align-items-center">
+ <div class="login-section">
+ <div class="login-section-bg">
+ <form #loginForm="ngForm" (ngSubmit)="login()">
+ <app-error
+ [error]="error"
+ authError="Invalid username/password">
+ </app-error>
+
+ <div class="form-group">
+ <input #apiEndpointInp="ngModel" [(ngModel)]="apiEndpoint"
+ [disabled]="loading"
+ autofocus
+ class="form-control"
+ id="apiEndpoint"
+ name="apiEndpoint"
+ placeholder="Server Address"
+ required>
+ <div *ngIf="!loading && !(apiEndpointInp.valid || apiEndpointInp.pristine)" class="alert alert-danger">
+ Value required.
+ </div>
+ </div>
+
+ <div class="form-group">
+ <input #emailInp="ngModel" [(ngModel)]="email"
+ [disabled]="loading"
+ autofocus
+ class="form-control"
+ id="email"
+ name="email"
+ placeholder="Email"
+ required
+ type="email">
+ <div *ngIf="!loading && !(emailInp.valid || emailInp.pristine)" class="alert alert-danger">
+ Value required.
+ </div>
+ </div>
+
+ <div class="form-group">
+ <input #passwordInp="ngModel" [(ngModel)]="password"
+ [disabled]="loading"
+ class="form-control"
+ id="password"
+ name="password"
+ placeholder="Password"
+ required
+ type="password">
+ <div *ngIf="!loading && !(passwordInp.valid || passwordInp.pristine)" class="alert alert-danger">
+ Value required.
+ </div>
+ </div>
+
+ <button [disabled]="loginForm.invalid || loading" class="btn btn-lg btn-primary btn-block" type="submit">
+ <ng-container *ngIf="!loading">
+ Sign in
+ </ng-container>
+ <ng-container *ngIf="loading">
+ <fa-icon icon="spinner" pulse="true"></fa-icon>
+ Signing in...
+ </ng-container>
+ </button>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/src/app/ui/login/login.component.ts b/src/app/ui/login/login.component.ts
new file mode 100644
index 0000000..70badc6
--- /dev/null
+++ b/src/app/ui/login/login.component.ts
@@ -0,0 +1,65 @@
+import {OnInit} from '@angular/core'
+import {Component, ViewChild} from '@angular/core'
+import {NgForm} from '@angular/forms'
+import {RestUrlService} from '../../services/rest/rest-url.service'
+import {RouterService} from '../../services/router/router.service'
+import {SessionService} from '../../services/session/session.service'
+
+@Component({
+ selector: 'app-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit {
+ apiEndpoint: string
+
+ email: string
+
+ password: string
+
+ @ViewChild('loginForm')
+ private _loginForm: NgForm
+
+ private _error: any
+
+ private _loading = false
+
+ constructor(
+ private _restUrl: RestUrlService,
+ private _routing: RouterService,
+ private _sessions: SessionService
+ ) {
+ // No-op.
+ }
+
+ get error(): any {
+ return this._error
+ }
+
+ get loading(): boolean {
+ return this._loading
+ }
+
+ ngOnInit(): void {
+ this.apiEndpoint = this._restUrl.apiEndpoint
+ }
+
+ public async login() {
+ if (this._loginForm.valid) {
+ try {
+ this._loading = true
+ this._error = null
+
+ this._restUrl.apiEndpoint = this.apiEndpoint
+
+ await this._sessions.login(this.email, this.password).toPromise()
+
+ this._routing.goToMain()
+ } catch (e) {
+ this._error = e
+ } finally {
+ this._loading = false
+ }
+ }
+ }
+}
diff --git a/src/app/ui/main/chat/chat.component.css b/src/app/ui/main/chat/chat.component.css
new file mode 100644
index 0000000..4dbc5b1
--- /dev/null
+++ b/src/app/ui/main/chat/chat.component.css
@@ -0,0 +1,3 @@
+.chat-entry {
+ cursor: pointer;
+}
diff --git a/src/app/ui/main/chat/chat.component.html b/src/app/ui/main/chat/chat.component.html
new file mode 100644
index 0000000..0ee7891
--- /dev/null
+++ b/src/app/ui/main/chat/chat.component.html
@@ -0,0 +1,58 @@
+<div class="d-flex flex-column h-100">
+ <app-error [error]="error"></app-error>
+
+ <div class="mt-3 mb-2 mr-3 d-flex justify-content-end">
+ <select [(ngModel)]="selectedModelId" class="form-control mr-2">
+ <ng-container *ngFor="let m of allModels()">
+ <option [ngValue]="m.id">{{m.name}} ({{m.id}}:{{m.version}})</option>
+ </ng-container>
+ </select>
+ </div>
+
+ <ng-container *ngIf="hasModel">
+ <div class="mb-2 mr-3 d-flex justify-content-between">
+ <button (click)="clearConversation()" [disabled]="!selectedModelId" class="btn btn-sm btn-light" type="button">
+ <fa-icon icon="comment-slash"></fa-icon>
+ Reset Conversation Context
+ </button>
+
+ <button (click)="clear()" [disabled]="!selectedModelId" class="btn btn-sm btn-light" type="button">
+ <fa-icon icon="trash-alt"></fa-icon>
+ Clear
+ </button>
+ </div>
+
+
+ <div class="border rounded flex-grow-1 mb-3 mr-3 p-3 d-flex flex-column-reverse " style="overflow: auto; height: 0">
+ <div (click)="selectedQuery = s" *ngFor="let s of states" class="chat-entry">
+ <div class="alert alert-primary">
+ {{s.txt}}
+ </div>
+
+ <div *ngIf="s.status === 'QRY_ENLISTED'" class="alert alert-secondary">
+ <fa-icon class="mr-2" icon="spinner" pulse="true"></fa-icon>
+ Processing...
+ </div>
+
+ <ng-container *ngIf="s.status === 'QRY_READY'">
+ <div *ngIf="s.resBody" class="alert alert-success">
+ {{s.resBody}}
+ </div>
+ <div *ngIf="s.error" class="alert alert-danger">
+ {{s.error}}
+ </div>
+ </ng-container>
+ </div>
+ </div>
+
+ <form (ngSubmit)="ask()" name="askForm">
+ <div class="row ml-0 mr-3 mb-4">
+ <input [(ngModel)]="queryText"
+ class="form-control form-control-lg"
+ name="queryText"
+ placeholder="Ask..."
+ type="text">
+ </div>
+ </form>
+ </ng-container>
+</div>
diff --git a/src/app/ui/main/chat/chat.component.ts b/src/app/ui/main/chat/chat.component.ts
new file mode 100644
index 0000000..0483e5e
--- /dev/null
+++ b/src/app/ui/main/chat/chat.component.ts
@@ -0,0 +1,142 @@
+import {Component, Input, OnDestroy, OnInit} from '@angular/core'
+import {NlpModel, NlpProbe, NlpQueryState} from '../../../services/nlp/nlp.model'
+import {NlpService} from '../../../services/nlp/nlp.service'
+
+@Component({
+ selector: 'app-chat',
+ templateUrl: './chat.component.html',
+ styleUrls: ['./chat.component.css']
+})
+export class ChatComponent implements OnInit, OnDestroy {
+ queryText: string
+
+ selectedModelId: string
+
+ selectedQuery: NlpQueryState
+
+ @Input()
+ allProbes: NlpProbe[]
+
+ private _states: NlpQueryState[]
+
+ private _error: any
+
+ private _timer: number
+
+ constructor(
+ private _nlp: NlpService
+ ) {
+ // No-op.
+ }
+
+ get states(): NlpQueryState[] {
+ return this._states
+ }
+
+ get error(): any {
+ return this._error
+ }
+
+ get hasModel(): boolean {
+ return !!this.selectedModelId
+ }
+
+ async ngOnInit() {
+ this.trySelectDefaultModel()
+
+ await this.checkStatus()
+
+ this._timer = setInterval(async () => {
+ if (this._states && this._states.find(s => s.status === 'QRY_ENLISTED')) {
+ await this.checkStatus()
+ }
+ }, 1000)
+ }
+
+ ngOnDestroy(): void {
+ clearInterval(this._timer)
+ }
+
+ allModels(): NlpModel[] {
+ const allModels: NlpModel[] = []
+
+ this.allProbes.forEach(p => {
+ p.models.forEach(m => {
+ allModels.push(m)
+ })
+ })
+
+ return allModels
+ }
+
+ async checkStatus() {
+ try {
+ this._states = (await this._nlp.check().toPromise()).states
+
+ if (this.selectedQuery) {
+ this.selectQuery(this.selectedQuery.srvReqId)
+ }
+ } catch (e) {
+ this._error = e
+ }
+ }
+
+ async ask() {
+ const query = this.queryText.trim()
+
+ if (this.selectedModelId && query.length > 0) {
+ this.queryText = ''
+
+ try {
+ this._error = null
+ this.selectedQuery = null
+
+ const reqId = (await this._nlp.ask(query, this.selectedModelId).toPromise()).srvReqId
+
+ await this.checkStatus()
+
+ this.selectQuery(reqId)
+ } catch (e) {
+ this._error = e
+ }
+ }
+ }
+
+ async clear() {
+ if (this._states) {
+ try {
+ this._error = null
+
+ await this._nlp.cancel(this._states.map(it => it.srvReqId)).toPromise()
+ } catch (e) {
+ this._error = e
+ }
+
+ await this.checkStatus()
+ }
+ }
+
+ async clearConversation() {
+ if (this.selectedModelId) {
+ try {
+ this._error = null
+
+ await this._nlp.clearConversation(this.selectedModelId).toPromise()
+ } catch (e) {
+ this._error = e
+ }
+ }
+ }
+
+ private selectQuery(id: string) {
+ if (this._states) {
+ this.selectedQuery = this._states.find(it => it.srvReqId === id)
+ }
+ }
+
+ private trySelectDefaultModel() {
+ if (!this.hasModel && this.allModels() && this.allModels().length > 0) {
+ this.selectedModelId = this.allModels()[0].id
+ }
+ }
+}
diff --git a/src/app/ui/main/details/details.component.css b/src/app/ui/main/details/details.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/ui/main/details/details.component.css
diff --git a/src/app/ui/main/details/details.component.html b/src/app/ui/main/details/details.component.html
new file mode 100644
index 0000000..a56118d
--- /dev/null
+++ b/src/app/ui/main/details/details.component.html
@@ -0,0 +1,131 @@
+<div *ngIf="query" class="d-flex flex-column h-100">
+ <div class="mt-3">
+ <h4>{{query.txt}}</h4>
+
+ <hr/>
+
+ <dl class="row small">
+ <dt class="col-sm-3">Request ID:</dt>
+ <dd class="col-sm-9">{{query.srvReqId}}</dd>
+
+ <dt class="col-sm-3">Status:</dt>
+ <dd class="col-sm-9">{{query.status}}</dd>
+
+ <dt class="col-sm-3">Model ID:</dt>
+ <dd class="col-sm-9">{{query.mdlId}}</dd>
+
+ <ng-container *ngIf="query.status === 'QRY_READY'">
+ <ng-container *ngIf="!query.error">
+ <dt class="col-sm-3">Response Type:</dt>
+ <dd class="col-sm-9">{{query.resType}}</dd>
+
+ <dt class="col-sm-3">Response Body:</dt>
+ <dd class="col-sm-9">{{query.resBody}}</dd>
+ </ng-container>
+
+ <ng-container *ngIf="query.error">
+ <dt class="col-sm-3">Error Message:</dt>
+ <dd class="col-sm-9">{{query.error}}</dd>
+
+ <dt class="col-sm-3">Error Code:</dt>
+ <dd class="col-sm-9">{{query.errorCode}}</dd>
+ </ng-container>
+ </ng-container>
+ </dl>
+ </div>
+
+ <ng-container *ngIf="query.logHolder">
+ <ng-container *ngIf="query.logHolder.intents && query.logHolder.intents.length > 0">
+ <h4>Matching intents (sorted from best to worst match)</h4>
+
+ <div class="flex-grow-1 mb-2 border rounded" style="overflow: auto; height: 0">
+ <ng-container *ngFor="let i of query.logHolder.intents">
+ <table class="table table-bordered table-sm">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Terms</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>{{i.id}}</td>
+ <td class="p-0">
+ <table class="table table-bordered table-sm m-0">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Text</th>
+ <th>Groups</th>
+ </tr>
+ </thead>
+ <tbody>
+ <ng-container *ngFor="let t of i.tokensGroups | keyvalue">
+ <ng-container *ngFor="let g of t.value">
+ <tr>
+ <td>{{g.token.id}}</td>
+ <td>{{g.token.metadata['nlpcraft:nlp:normtext']}}</td>
+ <td>{{g.token.groups}}</td>
+ </tr>
+ </ng-container>
+ </ng-container>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-container>
+ </div>
+ </ng-container>
+
+ <ng-container *ngIf="query.logHolder.queryContext && query.logHolder.queryContext.variants">
+ <h4>Variants</h4>
+
+ <div class="flex-grow-1 border rounded" style="overflow: auto; height: 0">
+ <ng-container *ngFor="let vs of query.logHolder.queryContext.variants; let i = index">
+ <h6>Variant #{{i + 1}}</h6>
+
+ <table class="table table-bordered table-sm">
+ <thead class="thead-light">
+ <tr>
+ <th>#</th>
+ <th>text</th>
+ <th>lemma</th>
+ <th>pos</th>
+ <th>quoted</th>
+ <th>stop</th>
+ <th>dict</th>
+ <th>indexes</th>
+ <th>direct</th>
+ <th>sparsity</th>
+ <th>groups</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let v of vs">
+ <td>{{v.metadata['nlpcraft:nlp:index']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:origtext']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:lemma']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:pos']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:quoted']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:stopword']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:dict']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:wordindexes']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:direct']}}</td>
+ <td>{{v.metadata['nlpcraft:nlp:english']}}</td>
+ <td>{{v.groups}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-container>
+ </div>
+ </ng-container>
+ </ng-container>
+
+
+ <!-- TODO -->
+ <!-- <div class="card w-100" style="max-height: 300px; overflow: auto">-->
+ <!-- <pre>{{query | json}}</pre>-->
+ <!-- </div>-->
+</div>
diff --git a/src/app/ui/main/details/details.component.ts b/src/app/ui/main/details/details.component.ts
new file mode 100644
index 0000000..3538c62
--- /dev/null
+++ b/src/app/ui/main/details/details.component.ts
@@ -0,0 +1,12 @@
+import {Component, Input} from '@angular/core'
+import {NlpQueryState} from '../../../services/nlp/nlp.model'
+
+@Component({
+ selector: 'app-details',
+ templateUrl: './details.component.html',
+ styleUrls: ['./details.component.css']
+})
+export class DetailsComponent {
+ @Input()
+ query: NlpQueryState
+}
diff --git a/src/app/ui/main/main.component.css b/src/app/ui/main/main.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/app/ui/main/main.component.css
diff --git a/src/app/ui/main/main.component.html b/src/app/ui/main/main.component.html
new file mode 100644
index 0000000..8b256e8
--- /dev/null
+++ b/src/app/ui/main/main.component.html
@@ -0,0 +1,23 @@
+<app-load-spinner *ngIf="loading"></app-load-spinner>
+
+<div class="m-3">
+ <app-error [error]="error"></app-error>
+</div>
+
+<ng-container *ngIf="hasProbes">
+ <div class="main-content flex-column d-flex justify-content-start align-items-start">
+ <div *ngIf="!loading" class="d-flex flex-column align-self-stretch flex-fill">
+ <app-error [error]="error"></app-error>
+
+ <div *ngIf="hasProbes" class="row m-0 d-flex justify-content-around align-self-stretch flex-fill">
+ <div class="col-8">
+ <app-details [query]="chatComponent.selectedQuery"></app-details>
+ </div>
+
+ <div class="col-4 p-0">
+ <app-chat #chatComponent [allProbes]="allProbes"></app-chat>
+ </div>
+ </div>
+ </div>
+ </div>
+</ng-container>
diff --git a/src/app/ui/main/main.component.ts b/src/app/ui/main/main.component.ts
new file mode 100644
index 0000000..ee2c843
--- /dev/null
+++ b/src/app/ui/main/main.component.ts
@@ -0,0 +1,74 @@
+import {Component, OnDestroy, OnInit} from '@angular/core'
+import {NlpProbe} from '../../services/nlp/nlp.model'
+import {NlpService} from '../../services/nlp/nlp.service'
+
+@Component({
+ selector: 'app-main',
+ templateUrl: './main.component.html',
+ styleUrls: ['./main.component.css']
+})
+export class MainComponent implements OnInit, OnDestroy {
+ private _error: any
+
+ private _loading = false
+
+ private _allProbes: NlpProbe[]
+
+ private _timer: number
+
+ constructor(
+ private _nlp: NlpService
+ ) {
+ // No-op.
+ }
+
+ get error(): any {
+ return this._error
+ }
+
+ get loading(): boolean {
+ return this._loading
+ }
+
+ get hasProbes(): boolean {
+ return this._allProbes && this._allProbes.length > 0
+ }
+
+ get allProbes(): NlpProbe[] {
+ return this._allProbes
+ }
+
+ async ngOnInit() {
+ this._timer = setInterval(async () => {
+ await this.loadProbes()
+ }, 1000)
+
+ try {
+ this._loading = true
+
+ await this.loadProbes()
+ } finally {
+ this._loading = false
+ }
+ }
+
+ ngOnDestroy(): void {
+ clearInterval(this._timer)
+ }
+
+ private async loadProbes() {
+ try {
+ this._allProbes = (await this._nlp.allProbes().toPromise()).probes
+
+ if (!this.hasProbes) {
+ this._error = 'No NLPCraft Probe to talk to :('
+ } else {
+ this._error = null
+ }
+ } catch (e) {
+ this._allProbes = null
+
+ this._error = e
+ }
+ }
+}
diff --git a/src/app/ui/navbar/navbar.component.css b/src/app/ui/navbar/navbar.component.css
new file mode 100644
index 0000000..4564b7f
--- /dev/null
+++ b/src/app/ui/navbar/navbar.component.css
@@ -0,0 +1,9 @@
+.logo {
+ height: 25px;
+}
+
+.profile-name {
+ font-weight: normal;
+ padding-right: 20px;
+ color: #fff;
+}
diff --git a/src/app/ui/navbar/navbar.component.html b/src/app/ui/navbar/navbar.component.html
new file mode 100644
index 0000000..b4760ce
--- /dev/null
+++ b/src/app/ui/navbar/navbar.component.html
@@ -0,0 +1,15 @@
+<nav class="navbar navbar-dark bg-dark px-4 sticky-top">
+ <span class="navbar-brand">
+ <img alt="NLPCraft" class="logo" src="assets/logo.gif">
+ </span>
+ <div>
+ <ul *ngIf="loggedIn" class="navbar-nav ml-auto">
+ <li class="nav-item">
+ <a (click)="logout()" class="nav-link" href="javascript:">
+ <fa-icon icon="sign-out-alt"></fa-icon>
+ Logout
+ </a>
+ </li>
+ </ul>
+ </div>
+</nav>
diff --git a/src/app/ui/navbar/navbar.component.ts b/src/app/ui/navbar/navbar.component.ts
new file mode 100644
index 0000000..15cbf7a
--- /dev/null
+++ b/src/app/ui/navbar/navbar.component.ts
@@ -0,0 +1,28 @@
+import {Component} from '@angular/core'
+import {Session} from '../../services/session/session.model'
+import {SessionService} from '../../services/session/session.service'
+
+@Component({
+ selector: 'app-navbar',
+ templateUrl: './navbar.component.html',
+ styleUrls: ['./navbar.component.css']
+})
+export class NavbarComponent {
+ constructor(
+ private _session: SessionService
+ ) {
+ // No-op.
+ }
+
+ get session(): Session {
+ return this._session.get()
+ }
+
+ get loggedIn(): boolean {
+ return this._session.hasSession()
+ }
+
+ logout(): void {
+ this._session.logout()
+ }
+}
diff --git a/src/app/ui/utils/error.component.ts b/src/app/ui/utils/error.component.ts
new file mode 100644
index 0000000..e063f77
--- /dev/null
+++ b/src/app/ui/utils/error.component.ts
@@ -0,0 +1,27 @@
+import {Component, Input} from '@angular/core'
+
+@Component({
+ selector: 'app-error',
+ styles: [`
+ `],
+ template: `
+ <ngb-alert *ngIf="hasError" type="danger" [dismissible]="false">
+ {{errorMessage}}
+ </ngb-alert>
+ `
+})
+export class ErrorComponent {
+ @Input()
+ error: any
+
+ @Input()
+ authError: string
+
+ get hasError(): boolean {
+ return !!this.error
+ }
+
+ get errorMessage(): string {
+ return 'ERROR: ' + (this.error.message ? this.error.message : JSON.stringify(this.error))
+ }
+}
diff --git a/src/app/ui/utils/load-spinner.component.ts b/src/app/ui/utils/load-spinner.component.ts
new file mode 100644
index 0000000..0b090a9
--- /dev/null
+++ b/src/app/ui/utils/load-spinner.component.ts
@@ -0,0 +1,22 @@
+import {Component, Input} from '@angular/core'
+
+@Component({
+ selector: 'app-load-spinner',
+ styles: [`
+ `],
+ template: `
+ <div class="modal-body" style="text-align: center">
+ <fa-icon icon="spinner" pulse="true" size="2x mb-2 mt-2"></fa-icon>
+ <br/>
+ {{resolveMessage()}}
+ </div>
+ `
+})
+export class LoadSpinnerComponent {
+ @Input()
+ message: string
+
+ resolveMessage(): string {
+ return this.message ? this.message : 'Loading...'
+ }
+}
diff --git a/src/assets/logo.gif b/src/assets/logo.gif
new file mode 100644
index 0000000..59d7bf9
--- /dev/null
+++ b/src/assets/logo.gif
Binary files differ
diff --git a/src/browserslist b/src/browserslist
new file mode 100644
index 0000000..37371cb
--- /dev/null
+++ b/src/browserslist
@@ -0,0 +1,11 @@
+# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+#
+# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
+
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead
+not IE 9-11
\ No newline at end of file
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 0000000..f58cdf1
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+}
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 0000000..9f02a39
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,16 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+}
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..fc0f9f6
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>NLPCraft</title>
+ <base href="/">
+ <meta content="width=device-width, initial-scale=1" name="viewport">
+</head>
+
+<body class="d-flex flex-column h-100">
+
+<div class="d-flex flex-column align-items-stretch">
+ <app-root></app-root>
+</div>
+</body>
+</html>
diff --git a/src/karma.conf.js b/src/karma.conf.js
new file mode 100644
index 0000000..d650004
--- /dev/null
+++ b/src/karma.conf.js
@@ -0,0 +1,32 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, '../coverage/web-app'),
+ reports: ['html', 'lcovonly', 'text-summary'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false,
+ restartOnFileChange: true
+ });
+};
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..ae47946
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,14 @@
+import {enableProdMode} from '@angular/core'
+import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'
+import {AppModule} from './app/app.module'
+import {environment} from './environments/environment'
+
+if (environment.production) {
+ enableProdMode()
+}
+
+platformBrowserDynamic()
+ .bootstrapModule(AppModule)
+ .catch(err =>
+ console.error(err)
+ )
diff --git a/src/polyfills.ts b/src/polyfills.ts
new file mode 100644
index 0000000..ab8aa3f
--- /dev/null
+++ b/src/polyfills.ts
@@ -0,0 +1,62 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags.ts';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone' // Included with Angular CLI.
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..f48543c
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,14 @@
+@import '~bootstrap/dist/css/bootstrap.css';
+
+* {
+ box-sizing: border-box;
+}
+
+/*TODO: Should we use web-site colors for top bar?*/
+/*.navbar {*/
+/* background-image: linear-gradient(141deg, #6980fa 0%, #2cb5e8 51%, #4298fc 75%)*/
+/*}*/
+
+.main-content {
+ height: calc(100vh - 56px);
+}
diff --git a/src/test.ts b/src/test.ts
new file mode 100644
index 0000000..2226c6b
--- /dev/null
+++ b/src/test.ts
@@ -0,0 +1,18 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import {getTestBed} from '@angular/core/testing'
+import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'
+import {platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'
+import 'zone.js/dist/zone-testing'
+
+declare const require: any
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+)
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/)
+// And load the modules.
+context.keys().map(context)
diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json
new file mode 100644
index 0000000..ed0c0df
--- /dev/null
+++ b/src/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "types": []
+ },
+ "exclude": [
+ "test.ts",
+ "**/*.spec.ts"
+ ]
+}
diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json
new file mode 100644
index 0000000..0a65d17
--- /dev/null
+++ b/src/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0921176
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "module": "es2015",
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "target": "es5",
+ "typeRoots": [
+ "node_modules/@types"
+ ],
+ "lib": [
+ "es2018",
+ "dom"
+ ]
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..1c76f87
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,102 @@
+{
+ "extends": "tslint:recommended",
+ "rulesDirectory": [
+ "codelyzer"
+ ],
+ "rules": {
+ "semicolon": [
+ true,
+ "never"
+ ],
+ "indent": [
+ true,
+ "spaces",
+ 4
+ ],
+ "array-type": false,
+ "arrow-parens": false,
+ "deprecation": {
+ "severity": "warn"
+ },
+ "import-blacklist": [
+ true,
+ "rxjs/Rx"
+ ],
+ "interface-name": false,
+ "max-classes-per-file": false,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ {
+ "order": [
+ "static-field",
+ "instance-field",
+ "static-method",
+ "instance-method"
+ ]
+ }
+ ],
+ "no-consecutive-blank-lines": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-empty": false,
+ "no-inferrable-types": [
+ true,
+ "ignore-params"
+ ],
+ "no-non-null-assertion": true,
+ "no-redundant-jsdoc": true,
+ "no-switch-case-fall-through": true,
+ "no-use-before-declare": true,
+ "no-var-requires": false,
+ "object-literal-key-quotes": [
+ true,
+ "as-needed"
+ ],
+ "object-literal-sort-keys": false,
+ "ordered-imports": true,
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "trailing-comma": false,
+ "no-output-on-prefix": true,
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "component-class-suffix": true,
+ "directive-class-suffix": true,
+ "variable-name": [
+ true,
+ "ban-keywords",
+ "check-format",
+ "allow-leading-underscore"
+ ],
+ "directive-selector": [
+ true,
+ "attribute",
+ "app",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ "element",
+ "app",
+ "kebab-case"
+ ]
+ }
+}