diff options
14 files changed, 363 insertions, 55 deletions
diff --git a/backend/microservice/api.py b/backend/microservice/api.py index b2fb503b..4768f34c 100644 --- a/backend/microservice/api.py +++ b/backend/microservice/api.py @@ -8,7 +8,9 @@ import keras import csv import json import mlservice -from mlservice import obuka +import h5py +from mlservice2 import unositok + app = flask.Flask(__name__) app.config["DEBUG"] = True @@ -21,8 +23,17 @@ def index(): @app.route('/data', methods = ['GET', 'POST']) def data(): if request.method == 'POST': - f = request.json['filepath'] - data = pd.read_csv(f) - print(data) - return obuka(data,request.json) + print(request.json['filepath']) + f = request.json['filepath'] + + data1 = pd.read_csv(f) + + d2=request.json['filepath2'] + data2=pd.read_csv(d2) + + m=request.json['modelpath'] + model=tf.keras.models.load_model(m) + + #print(data) + return unositok(data1,data2,request.json,model) app.run()
\ No newline at end of file diff --git a/backend/microservice/mlservice.py b/backend/microservice/mlservice.py index 3a0d41bd..b2eafe9a 100644 --- a/backend/microservice/mlservice.py +++ b/backend/microservice/mlservice.py @@ -1,17 +1,19 @@ -#from typing_extensions import Self +from statistics import mode +from typing_extensions import Self import pandas as pd import tensorflow as tf import keras import numpy as np - +import matplotlib.pyplot as plt from copyreg import constructor import flask from flask import request, jsonify, render_template from sklearn.preprocessing import LabelEncoder import csv import json +import h5py class Response: - def __init__(self,tacnost,preciznost,recall,spec,f1,mse,mae,mape,rmse,fpr,tpr): + def __init__(self,tacnost,preciznost,recall,spec,f1,mse,mae,mape,rmse): self.tacnost=tacnost self.preciznost=preciznost @@ -22,8 +24,7 @@ class Response: self.mae=mae self.mape=mape self.rmse=rmse - self.fpr=fpr - self.tpr=tpr + class fCallback(tf.keras.callbacks.Callback): def __init__(self, x_test, y_test): self.x_test = x_test @@ -31,15 +32,20 @@ class fCallback(tf.keras.callbacks.Callback): def on_epoch_end(self, epoch, logs=None): - print('Evaluation: ', self.model.evaluate(self.x_test)) + print('Evaluation: ', self.model.evaluate(self.x_test,self.y_test),"\n")#broj parametara zavisi od izabranih metrika loss je default - -def obuka(dataunos,params): +def obuka(dataunos,params,modelunos,dataunosdrugog): import numpy as np import pandas as pd import tensorflow as tf import matplotlib.pyplot as plt - + import keras + ### -1) Ucitavanje h5 modela PART3 + + if(modelunos!=None): + print("Model je unet") + model=modelunos + ### 0) Pretvaranje data seta u novi, sa kolonama koje je korisnik izabrao za obuku data=pd.DataFrame() @@ -48,6 +54,7 @@ def obuka(dataunos,params): data[zeljenekolone[i]]=dataunos[zeljenekolone[i]] #print(data.head(10)) + #predvidetikol=input("UNETI NAZIV KOLONE ČIJU VREDNOST TREBA PREDVIDETI ") ###sta se cuva od promenjivih broj kolone ili naziv kolone??? predvidetikol=params["columnToPredict"] @@ -69,7 +76,7 @@ def obuka(dataunos,params): ### 2)Proveravanje svih kolona za null vrednosti i popunjavanje medijanom ili srednjom vrednosti ili birisanje #####Part2 ##### - + ''' #brisanje=input("DA LI ZELITE DA IZBRSETE SVE KOLONE SA NULL VREDNOSTIMA? ") brisanje='ne' if(brisanje=='da'): @@ -102,37 +109,97 @@ def obuka(dataunos,params): elif(tippodataka==np.object_): najcescavrednost=kolona.value_counts().index[0] data[kolone[i]]=data[kolone[i]].fillna(najcescavrednost) - + ''' + + nullreplace=[ + {"column":"Embarked","value":"C","deleteRow":"0","deleteCol":"0"}, + {"column": "Cabin","value":"C123","deleteRow":"0","deleteCol":"0"}] + + + nullopt=params["NullValueOptions"] + + zamena=nullreplace + + nulldf=pd.DataFrame(zamena) + nulldf=nulldf.transpose() + nredova=data.shape[0] + if(nullopt=='replace'): + + + p=0 + + while(1): + if(p in nulldf.columns): + print("3") + parametri=nulldf[p] + print(parametri) + #print(data[parametri['column']]) + col=parametri['column'] + print(col) + val=parametri['value'] + print(val) + if(data[col].isnull().any()): + + if(parametri['value']!='0'): + + print(data[parametri['column']]) + data[parametri['column']]=data[parametri['column']].fillna(val) + print(data[parametri['column']]) + elif(parametri['deleteRow']==1): + data=data.dropna(subset=[col]) + print("brisi") + + + elif(parametri['deleteCol']==1): + data.pop(col) + p+=1 + continue + else: + break + + elif(nullopt=='deleteRow'): + data=data.dropna() + + elif(nullopt=='deleteCol'): + data=data.dropna() + + print(data.isnull().any()) + + + kolone=data.columns + print("null done") ### 3)Izbacivanje kolona koje ne uticu na rezultat PART2 nredova=data.shape[0] + for i in range(len(kolone)): - if((data[kolone[i]].nunique()>(nredova/2)) and( data[kolone[i]].dtype==np.object_)): + if((data[kolone[i]].nunique()==(nredova)) and( data[kolone[i]].dtype==np.object_)): data.pop(kolone[i]) #print(data.head(10)) ### 4)izbor tipa enkodiranja kolone=data.columns ### Azuriranje postojecih kolona nakon moguceg brisanja - + #enc=input("UNETI TIP ENKODIRANJA ") enc=params["encoding"] - onehot=0 - + + ### 5)Enkodiranje svih kategorijskih promenjivih label-encode metodom - + if(enc=='label'): + from sklearn.preprocessing import LabelEncoder encoder=LabelEncoder() for k in range(len(kolone)): if(data[kolone[k]].dtype==np.object_): data[kolone[k]]=encoder.fit_transform(data[kolone[k]]) #print(data.head(20)) - + ### 6)Enkodiranje svih kategorijskih promenjivih onehot metodom elif(enc=='onehot'): - ### PART2### - onehot==1 + ### PART2 ### + kategorijskekolone=[] for k in range(len(kolone)): if(data[kolone[k]].dtype==np.object_): @@ -140,7 +207,7 @@ def obuka(dataunos,params): kategorijskekolone.append(kolone[k]) ###U kategorijske kolone smestaju se nazivi svih kolona sa kategorijskim podacima #print(kategorijskekolone) - + ### Enkodiranje data=pd.get_dummies(data,columns=kategorijskekolone,prefix=kategorijskekolone) #print(data.head(10)) @@ -191,8 +258,10 @@ def obuka(dataunos,params): x_test=scaler.transform(x_test) x_train=scaler.transform(x_train) + ### 9)CUVANJE IZLAZNIH PODATAKA PART3 + #####ZAVRSENA PRIPREMA PODATAKA##### - + #####OBUCAVANJE MODELA##### ### 9)Inicijalizacija vestacke neuronske mreze @@ -208,7 +277,7 @@ def obuka(dataunos,params): classifier.add(tf.keras.layers.Dense(units=brojnu,activation=aktivacijau,input_dim=x_train.shape[1])) - ### 11)Dodavanje drugog, skrivenog sloja + ### 11)Dodavanje drugog, skrivenog sloja ###PART2### #aktivacijas=input("UNETI ŽELJENU AKTIVACIONU FUNKCIJU SKRIVENOG SLOJA ") #brojns=int(input("UNETI BROJ NEURONA SKRIVENOG SLOJA ")) @@ -234,16 +303,11 @@ def obuka(dataunos,params): optimizator=params["optimizer"] ### 13.1)Izbor metrike za kompajler PART2 - metrike=['mae','mse'] + metrike=params['metrics'] + #metrike=[] lossf=params["lossFunction"] - ''' - while(1): - m=params['lossFunction'] - - if(m=='KRAJ'): - break - metrike.append(m)''' - classifier.compile(optimizer=optimizator, loss=lossf,metrics =metrike) + + classifier.compile(optimizer=optimizator, loss=lossf,metrics=metrike) performance_simple = fCallback(x_test, y_test) ### 14) #uzorci=int(input("UNETI KOLIKO UZORAKA ĆE BITI UNETO U ISTO VREME ")) @@ -251,14 +315,14 @@ def obuka(dataunos,params): uzorci=params["batchSize"] epohe=params["epochs"] history=classifier.fit(x_train,y_train,batch_size=uzorci,epochs=epohe,callbacks=[performance_simple],validation_split=0.2) - + ### 14.1)Parametri grafika iz history PART2 metrikedf=pd.DataFrame() ###DataFrame u kom se nalaze podaci o rezultatima metrika za iscrtavanje na grafiku. Svaka kolona sadrzi vrednost metrike po epohama for i in range(len(metrike)): metrikedf[metrike[i]]=history.history[metrike[i]] #print(history.history[metrike[i]]) #plt.plot(history.history[metrike[i]]) - #plt.show() + plt.show() #print(metrikedf) @@ -278,13 +342,20 @@ def obuka(dataunos,params): #print(y_test) ### 15.2) Kreiranje DataFrame-a u kom se nalaze kolone koje predstavljaju stvarne i predvidjene vrednosti, potrebne za iscrtavanje grafika i metrike PART2 - #rezultat=pd.DataFrame({"Stvarna vrednost ":y_test,"Predvidjena vrednost":y_pred}) + rezultatzametrike=pd.DataFrame({"Stvarna vrednost ":y_test,"Predvidjena vrednost":y_pred}) #print(rezultat.head(20)) + ##### H5 CUVANJE ##### PART3 + nazivmodela=params['h5ModelName'] + classifier.save(nazivmodela, save_format='h5') + + + + #####METRIKE##### PART2 import sklearn.metrics as sm - + ### 16)Tacnost tacnost=sm.accuracy_score(y_test,y_pred) @@ -349,9 +420,128 @@ def obuka(dataunos,params): plt.ylabel('True Positive Rate') plt.show() ''' + + + + r=Response(float(tacnost),float(preciznost),float(recall),float(spec),float(f1),float(mse),float(mae),float(mape),float(rmse)) + import jsonpickle + return json.dumps(json.loads(jsonpickle.encode(r)), indent=2) +#####KRAJ OBUKE JEDNOG##### + + +##### UCITAVANJE I OBUKA DRUGOG SETA PODATAKA ##### PART3 +def ucitavanjeipreprocesiranjedrugog(dataunosdrugog,params): + data2=dataunosdrugog.copy() - r=Response(tacnost,preciznost,recall,spec,f1,mse,mae,mape,rmse,fpr,tpr) + + ### 1)Unos drugog seta i odstranjivanje nezeljenih kolona + data2=pd.DataFrame() + zeljenekolone2=params["inputColumns"] + for i in range(len(zeljenekolone2)): + data2[zeljenekolone2[i]]=dataunosdrugog[zeljenekolone2[i]] + + ### 2)Izbor kolona + kolone=data2.columns + + ### 3)NULL vrednosti + nullreplace=[ + {"column":"Embarked","value":"C","deleteRow":"0","deleteCol":"0"}, + {"column": "Cabin","value":"C123","deleteRow":"0","deleteCol":"0"}] + + + nullopt=params["NullValueOptions"] + + zamena=nullreplace + + nulldf=pd.DataFrame(zamena) + nulldf=nulldf.transpose() - return "Done" + if(nullopt=='replace'): + + + p=0 + + while(1): + if(p in nulldf.columns): + print("3") + parametri=nulldf[p] + print(parametri) + #print(data[parametri['column']]) + col=parametri['column'] + print(col) + + if(data2[col].isnull().any()): + + #print(parametri['value']) + if(parametri['value']!=''): + data2[col]=data2[col].fillna(parametri["value"]) + + elif(parametri['deleteRow']==1): + data2=data2.dropna(subset=[col]) + print("brisi") + + + elif(parametri['deleteCol']==1): + data2.pop(col) + p+=1 + continue + else: + break + + elif(nullopt=='deleteRow'): + data2=data2.dropna() + + elif(nullopt=='deleteCol'): + data2=data2.dropna() + + kolone=data2.columns + + ### 4)Enkodiranje + enc=params["encoding"] + + ### 5)Label + + if(enc=='label'): + from sklearn.preprocessing import LabelEncoder + encoder2=LabelEncoder() + for k in range(len(kolone)): + if(data2[kolone[k]].dtype==np.object_): + data2[kolone[k]]=encoder2.fit_transform(data2[kolone[k]]) + #print(data.head(20)) + + ### 6)Onehot + + elif(enc=='onehot'): + ### PART2### + + kategorijskekolone=[] + for k in range(len(kolone)): + if(data2[kolone[k]].dtype==np.object_): + + kategorijskekolone.append(kolone[k]) ###U kategorijske kolone smestaju se nazivi svih kolona sa kategorijskim podacima + + #print(kategorijskekolone) + + ### Enkodiranje + data2=pd.get_dummies(data2,columns=kategorijskekolone,prefix=kategorijskekolone) + #print(data.head(10)) + predvidetikol=params["columnToPredict"] + kolone=data2.columns ### Azuriranje kolona nakon moguceg dodavanja + xkolone=[] + for k in range(len(kolone)): + if(kolone[k]!=predvidetikol): + xkolone.append(kolone[k]) + + x2=data2[xkolone].values() + print(x2) + return x2 + #####OBUCAVANJE MODELA##### + + +def unositok(dataunos,dataunosdrugi,params,model): + data=obuka(dataunos,params,model,dataunosdrugi) + return(data) + +
\ No newline at end of file diff --git a/frontend/src/app/_data/Model.ts b/frontend/src/app/_data/Model.ts index 07364564..f6e01d08 100644 --- a/frontend/src/app/_data/Model.ts +++ b/frontend/src/app/_data/Model.ts @@ -107,7 +107,7 @@ export enum NullValueOptions { } export enum ReplaceWith { - None = '...', + None = 'Popuni...', Mean = 'Srednja vrednost', Median = 'Medijana' }
\ No newline at end of file diff --git a/frontend/src/app/_elements/item-model/item-model.component.css b/frontend/src/app/_elements/item-model/item-model.component.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/frontend/src/app/_elements/item-model/item-model.component.css diff --git a/frontend/src/app/_elements/item-model/item-model.component.html b/frontend/src/app/_elements/item-model/item-model.component.html new file mode 100644 index 00000000..ca170b3d --- /dev/null +++ b/frontend/src/app/_elements/item-model/item-model.component.html @@ -0,0 +1,26 @@ +<!-- +<div class="card" style="min-width: 12rem;"> + <div class="card-header"> + {{model.name}} + </div> + <div class="card-body"> + <p class="card-text"> + {{model.description}} + </p> + <div class="d-flex flex-column align-items-center"> + <table class="table table-bordered table-sm"> + <thead> + <th class="text-center" *ngFor="let column of model.inputs">{{column}}</th> + </thead> + </table> + <mat-icon>arrow_downward</mat-icon> + <p> + {{model.output}} + </p> + </div> + </div> + <div class="card-footer text-center"> + <a routerLink="predict" mat-raised-button color="primary">Iskoristi</a> + </div> +</div> +-->
\ No newline at end of file diff --git a/frontend/src/app/_elements/item-model/item-model.component.spec.ts b/frontend/src/app/_elements/item-model/item-model.component.spec.ts new file mode 100644 index 00000000..f696a160 --- /dev/null +++ b/frontend/src/app/_elements/item-model/item-model.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ItemModelComponent } from './item-model.component'; + +describe('ItemModelComponent', () => { + let component: ItemModelComponent; + let fixture: ComponentFixture<ItemModelComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ItemModelComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemModelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_elements/item-model/item-model.component.ts b/frontend/src/app/_elements/item-model/item-model.component.ts new file mode 100644 index 00000000..7f85f43f --- /dev/null +++ b/frontend/src/app/_elements/item-model/item-model.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit } from '@angular/core'; +import Model from 'src/app/_data/Model'; + +@Component({ + selector: 'app-item-model', + templateUrl: './item-model.component.html', + styleUrls: ['./item-model.component.css'] +}) +export class ItemModelComponent implements OnInit { + + @Input() model: Model = new Model(); + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/_modals/login-modal/login-modal.component.ts b/frontend/src/app/_modals/login-modal/login-modal.component.ts index 1b634c9a..c86c269a 100644 --- a/frontend/src/app/_modals/login-modal/login-modal.component.ts +++ b/frontend/src/app/_modals/login-modal/login-modal.component.ts @@ -2,6 +2,8 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; import { CookieService } from 'ngx-cookie-service'; import { AuthService } from 'src/app/_services/auth.service'; +import { UserInfoService } from 'src/app/_services/user-info.service'; +import shared from '../../Shared'; @Component({ selector: 'app-login-modal', @@ -18,7 +20,8 @@ export class LoginModalComponent implements OnInit { constructor( private authService: AuthService, private cookie: CookieService, - private router: Router + private router: Router, + private userInfoService: UserInfoService ) { } ngOnInit(): void { @@ -36,6 +39,9 @@ export class LoginModalComponent implements OnInit { else { this.authService.authenticate(response); (<HTMLSelectElement>document.getElementById('closeButton')).click(); + this.userInfoService.getUserInfo().subscribe((response) => { + shared.photoId = response.photoId; + }); } }); } diff --git a/frontend/src/app/_pages/add-model/add-model.component.html b/frontend/src/app/_pages/add-model/add-model.component.html index afd4ceb4..7e944a19 100644 --- a/frontend/src/app/_pages/add-model/add-model.component.html +++ b/frontend/src/app/_pages/add-model/add-model.component.html @@ -96,12 +96,12 @@ <input type="radio" [(ngModel)]="newModel.nullValues" [value]="NullValueOptions.DeleteRows" class="form-check-input" value="deleteRows" name="fillMissing" id="delRows" checked data-bs-toggle="collapse" data-bs-target="#fillMissingCustom.show"> - <label for="delRows" class="form-check-label">Obriši + <label for="delRows" class="form-check-label">Obriši sve redove sa nedostajućim vrednostima</label><br> <input type="radio" [(ngModel)]="newModel.nullValues" [value]="NullValueOptions.DeleteColumns" class="form-check-input" value="deleteCols" name="fillMissing" id="delCols" data-bs-toggle="collapse" data-bs-target="#fillMissingCustom.show"> - <label for="delCols" class="form-check-label">Obriši + <label for="delCols" class="form-check-label">Obriši sve kolone sa nedostajućim vrednostima</label><br> <input type="radio" [(ngModel)]="newModel.nullValues" [value]="NullValueOptions.Replace" class="form-check-input" name="fillMissing" id="replace" data-bs-toggle="collapse" @@ -114,11 +114,11 @@ <div id="columnReplacers"> <div *ngFor="let column of selectedDataset.header; let i = index" class="my-3"> <div class="input-group row" *ngIf="getInputById('cb_'+column).checked"> - <span class="input-group-text col-4 text-center"> + <span class="input-group-text col-2 text-center"> {{column}} </span> - <input type="text" class="form-control col-4"> - <select [id]="'replaceOptions'+i" class="form-control col-4" + <input type="text" class="form-control col-2"> + <select [id]="'replaceOptions'+i" class="form-control col-2" *ngIf="isNumber(datasetFile[1][i])"> <option *ngFor="let option of Object.keys(ReplaceWith); let optionName of Object.values(ReplaceWith)" @@ -126,6 +126,19 @@ {{ optionName }} </option> </select> + <select [id]="'replaceOptions'+i" class="form-control col-2" + *ngIf="!isNumber(datasetFile[1][i])"> + <option *ngFor="let option of arrayColumn(datasetFile, i)" + [value]="option"> + {{ option }} + </option> + </select> + <label class="form-control col-2" [for]="'delCol_'+column">Izbriši kolonu + <input type="radio" [id]="'delCol_'+column" + [name]="'delOp_'+column"></label> + <label class="form-control col-2" [for]="'delRows_'+column">Izbriši redove + <input type="radio" [id]="'delRows_'+column" [name]="'delOp_'+column" + checked></label> </div> </div> </div> diff --git a/frontend/src/app/_pages/add-model/add-model.component.ts b/frontend/src/app/_pages/add-model/add-model.component.ts index 156e51bc..fcc8ea70 100644 --- a/frontend/src/app/_pages/add-model/add-model.component.ts +++ b/frontend/src/app/_pages/add-model/add-model.component.ts @@ -271,8 +271,9 @@ export class AddModelComponent implements OnInit { !isNaN(Number(value.toString()))); } - getInputById(id: string): HTMLInputElement { return document.getElementById(id) as HTMLInputElement; } + + arrayColumn = (arr: any[][], n: number) => [...new Set(arr.map(x => x[n]))]; } diff --git a/frontend/src/app/_pages/my-models/my-models.component.html b/frontend/src/app/_pages/my-models/my-models.component.html index e69de29b..e94f67f5 100644 --- a/frontend/src/app/_pages/my-models/my-models.component.html +++ b/frontend/src/app/_pages/my-models/my-models.component.html @@ -0,0 +1,5 @@ +<ul class="list-group my-2"> + <li class="list-group-item" *ngFor="let model of myModels"> + <app-item-model [model]="model"></app-item-model> + </li> +</ul>
\ No newline at end of file diff --git a/frontend/src/app/_pages/my-models/my-models.component.ts b/frontend/src/app/_pages/my-models/my-models.component.ts index e9bc52de..3ab57e59 100644 --- a/frontend/src/app/_pages/my-models/my-models.component.ts +++ b/frontend/src/app/_pages/my-models/my-models.component.ts @@ -1,15 +1,22 @@ import { Component, OnInit } from '@angular/core'; +import Model from 'src/app/_data/Model'; @Component({ selector: 'app-my-models', templateUrl: './my-models.component.html', styleUrls: ['./my-models.component.css'] }) -export class MyModelsComponent implements OnInit { +export class MyModelsComponent /*implements OnInit*/ { + myModels: Model[]; - constructor() { } + constructor() { + this.myModels = [ + new Model('Titanik', 'Opis titanik'), + new Model('Neki drugi set', 'opis'), + new Model('Treci set', 'opis') + ]; } - ngOnInit(): void { + /*ngOnInit(): void { } - +*/ } diff --git a/frontend/src/app/_services/models.service.ts b/frontend/src/app/_services/models.service.ts index f629fd2a..d0346c03 100644 --- a/frontend/src/app/_services/models.service.ts +++ b/frontend/src/app/_services/models.service.ts @@ -42,4 +42,8 @@ export class ModelsService { getMyDatasets(): Observable<Dataset[]> { return this.http.get<Dataset[]>(`${API_SETTINGS.apiURL}/dataset/mydatasets`, { headers: this.authService.authHeader() }); } + + getMyModels(): Observable<Model[]> { + return this.http.get<Model[]>(`${API_SETTINGS.apiURL}/model/mymodels`, { headers: this.authService.authHeader() }); + } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 4612e3a7..0531a958 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -36,6 +36,7 @@ import { BarchartComponent } from './barchart/barchart.component'; import { NotificationsComponent } from './_elements/notifications/notifications.component'; import { DatatableComponent } from './_elements/datatable/datatable.component'; import { FilterDatasetsComponent } from './_pages/filter-datasets/filter-datasets.component'; +import { ItemModelComponent } from './_elements/item-model/item-model.component'; @NgModule({ declarations: [ @@ -61,7 +62,8 @@ import { FilterDatasetsComponent } from './_pages/filter-datasets/filter-dataset BarchartComponent, NotificationsComponent, DatatableComponent, - FilterDatasetsComponent + FilterDatasetsComponent, + ItemModelComponent ], imports: [ BrowserModule, |