juliethecao commented on code in PR #5675: URL: https://github.com/apache/texera/pull/5675#discussion_r3454250510
########## frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts: ########## @@ -0,0 +1,637 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Component, OnInit, OnDestroy, ChangeDetectorRef } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule } from "@angular/forms"; +import { FieldType, FieldTypeConfig, FormlyModule } from "@ngx-formly/core"; +import { HttpClient } from "@angular/common/http"; +import { NzSelectModule } from "ng-zorro-antd/select"; +import { NzInputModule } from "ng-zorro-antd/input"; +import { NzSpinModule } from "ng-zorro-antd/spin"; +import { NzButtonModule } from "ng-zorro-antd/button"; +import { NzIconModule } from "ng-zorro-antd/icon"; +import { AppSettings } from "../../../common/app-setting"; +import { Subject, Subscription } from "rxjs"; +import { debounceTime, finalize, switchMap, takeUntil } from "rxjs/operators"; + +export interface HuggingFaceModelOption { + id: string; + label: string; + pipeline_tag?: string; + downloads?: number; + likes?: number; +} + +export interface HuggingFaceTaskOption { + tag: string; + label: string; +} + +// ── Static fallback task list (used when the dynamic fetch fails) ── +export const STATIC_TASK_OPTIONS: HuggingFaceTaskOption[] = [ + { tag: "text-generation", label: "Text Generation" }, + { tag: "automatic-speech-recognition", label: "Automatic Speech Recognition" }, + { tag: "audio-classification", label: "Audio Classification" }, + { tag: "text-classification", label: "Text Classification" }, + { tag: "text-to-speech", label: "Text to Speech" }, + { tag: "token-classification", label: "Token Classification" }, + { tag: "question-answering", label: "Question Answering" }, + { tag: "table-question-answering", label: "Table Question Answering" }, + { tag: "zero-shot-classification", label: "Zero-Shot Classification" }, + { tag: "translation", label: "Translation" }, + { tag: "summarization", label: "Summarization" }, + { tag: "feature-extraction", label: "Feature Extraction" }, + { tag: "fill-mask", label: "Fill-Mask" }, + { tag: "sentence-similarity", label: "Sentence Similarity" }, + { tag: "text-ranking", label: "Text Ranking" }, + { tag: "image-classification", label: "Image Classification" }, + { tag: "object-detection", label: "Object Detection" }, + { tag: "image-segmentation", label: "Image Segmentation" }, + { tag: "image-to-text", label: "Image to Text" }, + { tag: "visual-question-answering", label: "Visual Question Answering" }, + { tag: "document-question-answering", label: "Document Question Answering" }, + { tag: "zero-shot-image-classification", label: "Zero-Shot Image Classification" }, +]; + +const PAGE_SIZE = 50; + +const TRUNCATED_HEADER = "X-Texera-Truncated"; + +// ── Module-level caches (reused across component instances) ── +const allModelsByTag: Map<string, HuggingFaceModelOption[]> = new Map(); +const truncatedByTag: Set<string> = new Set(); +const inFlightByTag: Map<string, Subscription> = new Map(); +const errorByTag: Map<string, string> = new Map(); + +let cachedTaskOptions: HuggingFaceTaskOption[] | null = null; +let tasksFetchSubscription: Subscription | null = null; +let tasksFetchError: string | null = null; + +/** Clear all cached data (useful for tests or manual invalidation). */ +export function invalidateHuggingFaceModelCache(): void { + allModelsByTag.clear(); + truncatedByTag.clear(); + errorByTag.clear(); + inFlightByTag.forEach(sub => sub.unsubscribe()); + inFlightByTag.clear(); + cachedTaskOptions = null; + tasksFetchError = null; + tasksFetchSubscription?.unsubscribe(); + tasksFetchSubscription = null; +} + +@Component({ + selector: "texera-hugging-face-model-select", + templateUrl: "./hugging-face.component.html", + styleUrls: ["hugging-face.component.scss"], + imports: [ + CommonModule, + FormsModule, + NzSelectModule, + NzInputModule, + NzSpinModule, + NzButtonModule, + NzIconModule, + FormlyModule, + ], +}) +export class HuggingFaceComponent extends FieldType<FieldTypeConfig> implements OnInit, OnDestroy { + private readonly taskScopedKeys = [ + "modelId", + "promptColumn", + "imageInput", + "audioInput", + "inputImageColumn", + "inputAudioColumn", + "candidateLabels", + "sentencesColumn", + "contextColumn", + "systemPrompt", + "maxNewTokens", + "temperature", + ] as const; + private readonly taskStateByTag = new Map<string, Partial<Record<(typeof this.taskScopedKeys)[number], unknown>>>(); + // ── Task state ── + taskOptions: HuggingFaceTaskOption[] = cachedTaskOptions ?? STATIC_TASK_OPTIONS; + selectedTaskTag = "text-generation"; + tasksLoading = false; + tasksError: string | null = null; + + // ── All models for the current task (fetched once from backend, cached) ── + private allModels: HuggingFaceModelOption[] = []; + + // ── Displayed state ── + pagedModels: HuggingFaceModelOption[] = []; + currentPage = 0; + totalPages = 0; + + loading = false; + errorMessage: string | null = null; + + // ── Truncation notice ── + truncated = false; + + // ── Search state ── + searchText = ""; + searchLoading = false; + private filteredModels: HuggingFaceModelOption[] | null = null; + private readonly searchSubject$ = new Subject<string>(); + private searchSubscription: Subscription | null = null; + + private readonly destroy$ = new Subject<void>(); + private subscription: Subscription | null = null; + private taskPollInterval: ReturnType<typeof setInterval> | null = null; + private modelPollInterval: ReturnType<typeof setInterval> | null = null; + private initTimeout: ReturnType<typeof setTimeout> | null = null; + + constructor( + private http: HttpClient, + private cdr: ChangeDetectorRef + ) { + super(); + } + + ngOnInit(): void { + const savedTag = this.getCurrentTaskTag(); + this.selectedTaskTag = savedTag ?? this.selectedTaskTag; + this.syncTaskSelection(this.selectedTaskTag, false); + this.loadTasks(); + this.loadAllModels(); + this.setupServerSearch(); + // Formly can attach sibling controls after this field initializes. + // Re-sync once the control tree settles so a fresh operator starts in a valid task state. + this.initTimeout = setTimeout( + () => this.syncTaskSelection(this.getCurrentTaskTag() ?? this.selectedTaskTag, false), + 0 + ); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.subscription?.unsubscribe(); + this.searchSubscription?.unsubscribe(); + this.searchSubject$.complete(); + if (this.taskPollInterval !== null) { + clearInterval(this.taskPollInterval); + } + if (this.modelPollInterval !== null) { + clearInterval(this.modelPollInterval); + } + if (this.initTimeout !== null) { + clearTimeout(this.initTimeout); + } + } + + // ── Task loading ── + + /** + * Fetch available pipeline tags from the backend, which proxies HuggingFace's /api/tasks. + * Falls back to STATIC_TASK_OPTIONS if the fetch fails. + */ + private loadTasks(): void { + // Already fetched and cached + if (cachedTaskOptions !== null) { + this.taskOptions = cachedTaskOptions; + return; + } + + // Previous fetch errored — show static list, don't retry automatically + if (tasksFetchError !== null) { + this.tasksError = tasksFetchError; + this.taskOptions = STATIC_TASK_OPTIONS; + return; + } + + // Another component instance already has a fetch in flight — wait for it + if (tasksFetchSubscription !== null) { + this.tasksLoading = true; + // Poll for completion (the module-level cache will be set when done) + this.taskPollInterval = setInterval(() => { + if (cachedTaskOptions !== null || tasksFetchError !== null) { + clearInterval(this.taskPollInterval!); + this.taskPollInterval = null; + this.tasksLoading = false; + this.taskOptions = cachedTaskOptions ?? STATIC_TASK_OPTIONS; + if (tasksFetchError) this.tasksError = tasksFetchError; + this.cdr.detectChanges(); + } + }, 200); + return; + } + + this.tasksLoading = true; + this.tasksError = null; + this.cdr.detectChanges(); + + tasksFetchSubscription = this.http + .get<HuggingFaceTaskOption[]>(`${AppSettings.getApiEndpoint()}/huggingface/tasks`) + .pipe( + takeUntil(this.destroy$), + finalize(() => { + // If takeUntil fires before next/error, reset the module-level guard + // so the next component instance can start a fresh fetch. + if (cachedTaskOptions === null && tasksFetchError === null) { + tasksFetchSubscription = null; + } + }) + ) + .subscribe({ Review Comment: Fix: `takeUntil(this.destroy$)` is removed only from the shared tasks fetch. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
