Skip to content
This repository was archived by the owner on Aug 28, 2023. It is now read-only.

Commit 9ffeaf9

Browse files
authored
[82172] [80754] Implement huggingface model details (#28)
1 parent 4eacc80 commit 9ffeaf9

21 files changed

+1088
-68
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ repos:
2525
files: '^client/'
2626
args: [
2727
'--write',
28-
'--loglevel', 'debug',
2928
'--list-different',
3029
'--ignore-unknown',
3130
'--config', './client/.prettierrc',
3231
'--ignore-path', './client/.prettierignore'
3332
]
33+
require_serial: true
3434
stages: [commit]

client/package-lock.json

+615
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"file-saver": "2.0.1",
4040
"hammerjs": "2.0.8",
4141
"handlebars": "4.7.7",
42+
"highlight.js": "11.5.0",
4243
"js-yaml": "4.0.0",
4344
"jwt-decode": "2.2.0",
4445
"lodash": "4.17.21",
@@ -47,6 +48,7 @@
4748
"material-design-icons": "3.0.1",
4849
"monaco-editor": "0.22.3",
4950
"papaparse": "5.3.1",
51+
"primer": "11.0.0",
5052
"protobufjs": "6.8.8",
5153
"rxjs": "6.6.3",
5254
"socket.io-client": "4.1.1",
@@ -73,6 +75,7 @@
7375
"@types/jasminewd2": "2.0.8",
7476
"@types/jwt-decode": "2.2.1",
7577
"@types/lodash": "4.14.149",
78+
"@types/marked": "4.0.2",
7679
"@types/node": "14.18.0",
7780
"@types/papaparse": "5.3.1",
7881
"@types/pixelmatch": "5.2.2",

client/src/app/modules/model-manager/components/hugging-face-import-ribbon-content/hugging-face-import-ribbon-content.component.html

+8-15
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,14 @@
4242
</wb-card-grid>
4343
</wb-model-zoo-content>
4444

45-
<wb-model-zoo-details>
46-
<wb-model-zoo-details-header
47-
[modelName]="selectedModel?.id"
48-
(hideDetails)="selectedModel = null"
49-
></wb-model-zoo-details-header>
50-
51-
<wb-model-zoo-details-footer>
52-
<wb-button
53-
type="primary"
54-
class="enlarged-control"
55-
text="Download and Import"
56-
(handleClick)="handleUploadModel()"
57-
></wb-button>
58-
</wb-model-zoo-details-footer>
59-
</wb-model-zoo-details>
45+
<wb-huggingface-model-details
46+
ngProjectAs="wb-model-zoo-details"
47+
[huggingfaceModel]="selectedModel"
48+
[tagsSets]="tagsSets"
49+
(import)="handleUploadModel()"
50+
(hide)="selectedModel = null"
51+
>
52+
</wb-huggingface-model-details>
6053
</wb-model-zoo-layout>
6154

6255
<ng-template #filtersTemplate>

client/src/app/modules/model-manager/components/hugging-face-import-ribbon-content/hugging-face-import-ribbon-content.component.ts

+29-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ import { ModelStoreActions, RootStoreState } from '@store';
2626
import { HuggingfaceModelZooDataSource } from '@shared/models/model-zoo-data-source/huggingface-model-zoo-data-source';
2727
import { IHuggingfaceModel } from '@shared/models/huggingface/huggingface-model';
2828

29+
export interface IHuggingfaceTagsSets {
30+
pipelineTags: Set<string>;
31+
libraries: Set<string>;
32+
languages: Set<string>;
33+
licenses: Set<string>;
34+
modelTypes: Set<string>;
35+
}
36+
2937
@Component({
3038
selector: 'wb-hugging-face-import-ribbon-content',
3139
templateUrl: './hugging-face-import-ribbon-content.component.html',
@@ -41,6 +49,9 @@ export class HuggingFaceImportRibbonContentComponent implements OnInit, AfterVie
4149

4250
appliedTags: IHuggingfaceAppliedModelTags = null;
4351
availableTags: IHuggingfaceAvailableTags = null;
52+
tagsSets: IHuggingfaceTagsSets = null;
53+
54+
idSearch = '';
4455

4556
readonly dataSource = new HuggingfaceModelZooDataSource();
4657

@@ -65,16 +76,21 @@ export class HuggingFaceImportRibbonContentComponent implements OnInit, AfterVie
6576
this.dataSource.data = models;
6677
this.appliedTags = applied;
6778
this.availableTags = available;
79+
this.tagsSets = {
80+
libraries: new Set(applied.libraries),
81+
pipelineTags: new Set(applied.pipelineTags),
82+
modelTypes: new Set(available.modelTypes),
83+
languages: new Set(available.languages),
84+
licenses: new Set(available.licenses),
85+
};
6886
this._cdr.detectChanges();
6987
});
7088

7189
this.sortControl.valueChanges
7290
.pipe(takeUntil(this._unsubscribe$))
73-
.subscribe((sort) => (this.dataSource.sort = sort));
91+
.subscribe((sort) => (this.dataSource.sort = { active: sort, direction: 'desc' }));
7492

75-
this.filterControl.valueChanges
76-
.pipe(takeUntil(this._unsubscribe$))
77-
.subscribe((value: IHuggingfaceAppliedModelTags) => (this.dataSource.filterTags = Object.values(value).flat()));
93+
this.filterControl.valueChanges.pipe(takeUntil(this._unsubscribe$)).subscribe(() => this._filter());
7894
}
7995

8096
ngAfterViewInit(): void {
@@ -87,10 +103,18 @@ export class HuggingFaceImportRibbonContentComponent implements OnInit, AfterVie
87103
}
88104

89105
searchModels(value: string): void {
90-
this.dataSource.filter = value;
106+
this.idSearch = value;
107+
this._filter();
91108
}
92109

93110
handleUploadModel(): void {
94111
this._store$.dispatch(ModelStoreActions.importHuggingfaceModel({ huggingface_model_id: this.selectedModel.id }));
95112
}
113+
114+
private _filter(): void {
115+
this.dataSource.filter = {
116+
id: this.idSearch,
117+
tags: Object.values(this.filterControl?.value || {}).flat() as string[],
118+
};
119+
}
96120
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<wb-model-zoo-details>
2+
<wb-model-zoo-details-header
3+
[modelName]="huggingfaceModel?.id"
4+
(hideDetails)="hide.emit()"
5+
></wb-model-zoo-details-header>
6+
7+
<wb-model-zoo-details-parameters>
8+
<wb-parameter-details class="parameter" *ngFor="let param of parameters" [parameter]="param"></wb-parameter-details>
9+
</wb-model-zoo-details-parameters>
10+
11+
<wb-model-zoo-details-description>
12+
<div class="readme markdown-body" [innerHTML]="readme"></div>
13+
</wb-model-zoo-details-description>
14+
15+
<wb-model-zoo-details-footer>
16+
<wb-button
17+
type="primary"
18+
class="enlarged-control"
19+
text="Download and Import"
20+
(handleClick)="import.emit()"
21+
></wb-button>
22+
</wb-model-zoo-details-footer>
23+
</wb-model-zoo-details>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
:host {
2+
display: flex;
3+
overflow: hidden;
4+
flex-direction: column;
5+
flex-grow: 1;
6+
}
7+
8+
.readme ::ng-deep {
9+
font-family: Roboto, sans-serif;
10+
11+
table {
12+
border-spacing: 0;
13+
border-collapse: collapse;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { HuggingfaceService } from '@core/services/api/rest/huggingface.service';
4+
5+
import { SharedModule } from '@shared/shared.module';
6+
7+
import { HuggingfaceModelDetailsComponent } from './huggingface-model-details.component';
8+
import { MarkdownService } from './markdown/markdown.service';
9+
10+
describe('HuggingfaceModelDetailsComponent', () => {
11+
let component: HuggingfaceModelDetailsComponent;
12+
let fixture: ComponentFixture<HuggingfaceModelDetailsComponent>;
13+
14+
beforeEach(async () => {
15+
await TestBed.configureTestingModule({
16+
imports: [SharedModule],
17+
providers: [HuggingfaceService, MarkdownService],
18+
declarations: [HuggingfaceModelDetailsComponent],
19+
}).compileComponents();
20+
});
21+
22+
beforeEach(() => {
23+
fixture = TestBed.createComponent(HuggingfaceModelDetailsComponent);
24+
component = fixture.componentInstance;
25+
fixture.detectChanges();
26+
});
27+
28+
it('should create', () => {
29+
expect(component).toBeTruthy();
30+
});
31+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
ChangeDetectorRef,
4+
Component,
5+
EventEmitter,
6+
Inject,
7+
Input,
8+
LOCALE_ID,
9+
Output,
10+
} from '@angular/core';
11+
import { DatePipe } from '@angular/common';
12+
13+
import { HuggingfaceService } from '@core/services/api/rest/huggingface.service';
14+
15+
import { ModelDomain, modelDomainNames } from '@store/model-store/model.model';
16+
17+
import { IHuggingfaceModel } from '@shared/models/huggingface/huggingface-model';
18+
import { IParameter } from '@shared/components/model-details/parameter-details/parameter-details.component';
19+
20+
import { MarkdownService } from './markdown/markdown.service';
21+
import { IHuggingfaceTagsSets } from '../hugging-face-import-ribbon-content.component';
22+
23+
@Component({
24+
selector: 'wb-huggingface-model-details',
25+
templateUrl: './huggingface-model-details.component.html',
26+
styleUrls: ['./huggingface-model-details.component.scss'],
27+
changeDetection: ChangeDetectionStrategy.OnPush,
28+
})
29+
export class HuggingfaceModelDetailsComponent {
30+
private _model: IHuggingfaceModel = null;
31+
32+
@Input() set huggingfaceModel(value: IHuggingfaceModel) {
33+
this._model = value;
34+
if (this._model) {
35+
this.parameters = this._extractHfModelParameters(this._model);
36+
(async () => {
37+
this.readme = await this._fetchReadme();
38+
this._cdr.detectChanges();
39+
})();
40+
} else {
41+
this.parameters = null;
42+
this.readme = null;
43+
}
44+
}
45+
46+
get huggingfaceModel(): IHuggingfaceModel {
47+
return this._model;
48+
}
49+
50+
@Input() tagsSets: IHuggingfaceTagsSets;
51+
52+
@Output() import = new EventEmitter<void>();
53+
@Output() hide = new EventEmitter<void>();
54+
55+
parameters: IParameter[] = [];
56+
readme: string = null;
57+
58+
constructor(
59+
private readonly _hfService: HuggingfaceService,
60+
private readonly _cdr: ChangeDetectorRef,
61+
private readonly _mdService: MarkdownService,
62+
@Inject(LOCALE_ID) private readonly _localeId: string
63+
) {}
64+
65+
private _extractTags(tags: string[], tag_group: Set<string>): string {
66+
return tags.filter((tag) => tag_group.has(tag)).join(', ') || 'N/A';
67+
}
68+
69+
private _extractHfModelParameters(model: IHuggingfaceModel): IParameter[] {
70+
return [
71+
{ label: 'Domain', value: modelDomainNames[ModelDomain.NLP], tooltip: 'Domain' },
72+
{ label: 'Library', value: this._extractTags(model.tags, this.tagsSets.libraries) },
73+
{ label: 'Tasks', value: this._extractTags(model.tags, this.tagsSets.pipelineTags) },
74+
{ label: 'Languages', value: this._extractTags(model.tags, this.tagsSets.languages) },
75+
{ label: 'Licenses', value: this._extractTags(model.tags, this.tagsSets.licenses) },
76+
{ label: 'Downloads', value: model.downloads },
77+
{ label: 'Updated', value: new DatePipe(this._localeId).transform(model.lastModified, 'YYYY/MM/dd, hh:mm') },
78+
];
79+
}
80+
81+
private async _fetchReadme(): Promise<string> {
82+
const markdown = await this._hfService.getModelDetails$(this._model.id).toPromise();
83+
return this._mdService.parse(markdown);
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface IMarkdownParserOptions {
2+
highlight: boolean;
3+
}
4+
5+
export interface IMarkdownParser {
6+
parse: (markdownTest: string, options?: IMarkdownParserOptions) => Promise<string>;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@import 'highlight.js/styles/github.css';
2+
@import 'primer-markdown/index';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { MarkdownService } from './markdown.service';
4+
5+
describe('MarkdownService', () => {
6+
let service: MarkdownService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(MarkdownService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Injectable, SecurityContext } from '@angular/core';
2+
import { DomSanitizer } from '@angular/platform-browser';
3+
4+
import { IMarkdownParser, IMarkdownParserOptions } from './index';
5+
6+
// todo: proposal to move all markdown logic to a module with a service and a component
7+
// component template `<div class="markdown-body" [innerHTML]="readme"></div>`
8+
@Injectable({
9+
providedIn: 'root',
10+
})
11+
export class MarkdownService {
12+
private _parser: IMarkdownParser;
13+
14+
constructor(private readonly _sanitizer: DomSanitizer) {}
15+
16+
private async _loadParser() {
17+
this._parser = await import('./markdown');
18+
}
19+
20+
async parse(text: string, options?: IMarkdownParserOptions): Promise<string> {
21+
await this._loadParser();
22+
const markdown = await this._parser.parse(text, options);
23+
return this._sanitizer.sanitize(SecurityContext.HTML, markdown);
24+
}
25+
}

0 commit comments

Comments
 (0)