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"
+        ]
+    }
+}