Ma77Ball commented on code in PR #5566:
URL: https://github.com/apache/texera/pull/5566#discussion_r3383901487


##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts:
##########
@@ -0,0 +1,543 @@
+/**
+ * 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 { 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" },
+];
+
+// Keep legacy export for any other code that imports it
+export const TASK_TAG_MAP: Record<string, string> = {};
+for (const { tag, label } of STATIC_TASK_OPTIONS) {
+  TASK_TAG_MAP[label] = tag;
+}
+export const TASK_NAMES = STATIC_TASK_OPTIONS.map(t => t.label);

Review Comment:
   `TASK_TAG_MAP` and `TASK_NAMES` have no consumers anywhere in the frontend 
(grep finds none across this stack), and the "legacy" comment is speculative. 
This is dead code that will drift from the real task list. Safe to remove (or 
if implemented later please ignore).
   
   ```suggestion
   ```



##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts:
##########
@@ -0,0 +1,543 @@
+/**
+ * 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 { 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" },
+];
+
+// Keep legacy export for any other code that imports it
+export const TASK_TAG_MAP: Record<string, string> = {};
+for (const { tag, label } of STATIC_TASK_OPTIONS) {
+  TASK_TAG_MAP[label] = tag;
+}
+export const TASK_NAMES = STATIC_TASK_OPTIONS.map(t => t.label);
+
+const PAGE_SIZE = 50;
+
+// ── Module-level caches (reused across component instances) ──
+const allModelsByTag: Map<string, HuggingFaceModelOption[]> = new Map();
+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();
+  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;
+
+  // ── Search state (client-side filtering over ALL models) ──
+  searchText = "";
+  private filteredModels: HuggingFaceModelOption[] | null = null;
+
+  private readonly destroy$ = new Subject<void>();
+  private subscription: Subscription | 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();
+    // 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.
+    setTimeout(() => this.syncTaskSelection(this.getCurrentTaskTag() ?? 
this.selectedTaskTag, false), 0);
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+    this.subscription?.unsubscribe();
+  }
+
+  // ── 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)
+      const poll = setInterval(() => {
+        if (cachedTaskOptions !== null || tasksFetchError !== null) {
+          clearInterval(poll);
+          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$))
+      .subscribe({
+        next: tasks => {
+          tasksFetchSubscription = null;
+          cachedTaskOptions = tasks.length > 0 ? tasks : STATIC_TASK_OPTIONS;
+          this.taskOptions = cachedTaskOptions;
+          this.tasksLoading = false;
+          this.cdr.detectChanges();
+        },
+        error: (err: unknown) => {
+          console.error("Failed to load HuggingFace tasks:", err);
+          tasksFetchSubscription = null;
+          tasksFetchError = "Could not load tasks from Hugging Face. Using 
default list.";
+          this.tasksError = tasksFetchError;
+          this.taskOptions = STATIC_TASK_OPTIONS;
+          this.tasksLoading = false;
+          this.cdr.detectChanges();
+        },
+      });
+  }
+
+  retryTasksLoad(): void {
+    tasksFetchError = null;
+    this.tasksError = null;
+    this.loadTasks();
+  }
+
+  // ── Task selection ──
+
+  onTaskSelected(tag: string): void {
+    const previousTask = this.getCurrentTaskTag() ?? this.selectedTaskTag;
+    this.snapshotTaskState(previousTask);
+    this.syncTaskSelection(tag, true);
+    this.restoreTaskState(tag);
+    this.searchText = "";
+    this.filteredModels = null;
+    this.loadAllModels();
+  }
+
+  // ── Data loading ──
+
+  /**
+   * Fetch ALL models for the selected task.
+   * The backend paginates through HF Hub internally and caches the result.
+   * The first request per task may be slow; subsequent requests are instant.
+   */
+  private loadAllModels(): void {
+    const tag = this.selectedTaskTag || "text-generation";
+
+    this.loading = false;
+    this.errorMessage = null;
+
+    // Fast path: cached on the frontend
+    if (allModelsByTag.has(tag)) {
+      this.allModels = allModelsByTag.get(tag)!;
+      this.goToPage(0);
+      return;
+    }
+
+    // Previous error
+    if (errorByTag.has(tag)) {
+      this.errorMessage = errorByTag.get(tag)!;
+      this.allModels = [];
+      this.pagedModels = [];
+      this.totalPages = 0;
+      return;
+    }
+
+    // Cancel previous
+    this.subscription?.unsubscribe();
+    this.subscription = null;
+
+    this.allModels = [];
+    this.pagedModels = [];
+    this.totalPages = 0;
+
+    // Show spinner immediately for the initial fetch — it can take a while
+    // as the backend pages through HF Hub for the first time.
+    this.loading = true;
+    this.cdr.detectChanges();
+
+    this.subscription = this.http
+      .get<HuggingFaceModelOption[]>(
+        
`${AppSettings.getApiEndpoint()}/huggingface/models?task=${encodeURIComponent(tag)}`
+      )
+      .pipe(takeUntil(this.destroy$))
+      .subscribe({
+        next: models => {
+          allModelsByTag.set(tag, models);
+          inFlightByTag.delete(tag);
+          this.loading = false;
+          this.allModels = models;
+          this.goToPage(0);
+        },
+        error: (err: unknown) => {
+          console.error(`Failed to load HuggingFace models for task 
'${tag}':`, err);
+          const msg = "Failed to load models. Click retry to try again.";
+          errorByTag.set(tag, msg);
+          inFlightByTag.delete(tag);
+          this.loading = false;
+          this.errorMessage = msg;
+          this.cdr.detectChanges();
+        },
+      });
+
+    inFlightByTag.set(tag, this.subscription);

Review Comment:
   `inFlightByTag` is written here and deleted in the next/error handlers, but 
it is never read to dedup concurrent fetches (only 
`invalidateHuggingFaceModelCache` iterates it). So unlike the tasks path, 
`loadAllModels` has no in-flight guard, and two instances mounting for the same 
uncached task each fire a full HF-Hub-paginating backend request. Either add an 
in-flight guard before fetching, or remove the map if dedup is not intended.



##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts:
##########
@@ -0,0 +1,543 @@
+/**
+ * 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 { 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" },
+];
+
+// Keep legacy export for any other code that imports it
+export const TASK_TAG_MAP: Record<string, string> = {};
+for (const { tag, label } of STATIC_TASK_OPTIONS) {
+  TASK_TAG_MAP[label] = tag;
+}
+export const TASK_NAMES = STATIC_TASK_OPTIONS.map(t => t.label);
+
+const PAGE_SIZE = 50;
+
+// ── Module-level caches (reused across component instances) ──
+const allModelsByTag: Map<string, HuggingFaceModelOption[]> = new Map();
+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();
+  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;
+
+  // ── Search state (client-side filtering over ALL models) ──
+  searchText = "";
+  private filteredModels: HuggingFaceModelOption[] | null = null;
+
+  private readonly destroy$ = new Subject<void>();
+  private subscription: Subscription | 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();
+    // 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.
+    setTimeout(() => this.syncTaskSelection(this.getCurrentTaskTag() ?? 
this.selectedTaskTag, false), 0);
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+    this.subscription?.unsubscribe();
+  }
+
+  // ── 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)
+      const poll = setInterval(() => {
+        if (cachedTaskOptions !== null || tasksFetchError !== null) {
+          clearInterval(poll);
+          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$))

Review Comment:
   This fetch is stored in a module-level (shared) variable and cached for all 
instances, but it is torn down by this instance's `destroy$`. If the initiator 
is destroyed before the response arrives, the shared fetch is cancelled with 
neither `next` nor `error` firing, leaving `tasksFetchSubscription` non-null 
and the cache null forever, so every later instance takes the polling branch 
above and polls indefinitely. The model fetch at line 309 has the same 
shared-vs-instance mismatch. Suggest not piping `takeUntil(this.destroy$)` on 
the shared fetches, or using a dedicated module-level teardown subject.



##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts:
##########
@@ -0,0 +1,543 @@
+/**
+ * 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 { 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" },
+];
+
+// Keep legacy export for any other code that imports it
+export const TASK_TAG_MAP: Record<string, string> = {};
+for (const { tag, label } of STATIC_TASK_OPTIONS) {
+  TASK_TAG_MAP[label] = tag;
+}
+export const TASK_NAMES = STATIC_TASK_OPTIONS.map(t => t.label);
+
+const PAGE_SIZE = 50;
+
+// ── Module-level caches (reused across component instances) ──
+const allModelsByTag: Map<string, HuggingFaceModelOption[]> = new Map();
+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();
+  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;
+
+  // ── Search state (client-side filtering over ALL models) ──
+  searchText = "";
+  private filteredModels: HuggingFaceModelOption[] | null = null;
+
+  private readonly destroy$ = new Subject<void>();
+  private subscription: Subscription | 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();
+    // 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.
+    setTimeout(() => this.syncTaskSelection(this.getCurrentTaskTag() ?? 
this.selectedTaskTag, false), 0);
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+    this.subscription?.unsubscribe();
+  }
+
+  // ── 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)
+      const poll = setInterval(() => {
+        if (cachedTaskOptions !== null || tasksFetchError !== null) {
+          clearInterval(poll);
+          this.tasksLoading = false;
+          this.taskOptions = cachedTaskOptions ?? STATIC_TASK_OPTIONS;
+          if (tasksFetchError) this.tasksError = tasksFetchError;
+          this.cdr.detectChanges();
+        }
+      }, 200);
+      return;

Review Comment:
   This `setInterval` handle is local and never cleared in `ngOnDestroy` (which 
only handles `destroy$` and `this.subscription`). If this instance is destroyed 
before the other instance's fetch resolves, the timer keeps firing and calls 
`detectChanges()` on a destroyed view. It also never terminates if the 
initiating fetch was cancelled (see the next comment). Store the id on the 
instance and `clearInterval` it in `ngOnDestroy`.



##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts:
##########
@@ -0,0 +1,543 @@
+/**
+ * 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 { 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" },
+];
+
+// Keep legacy export for any other code that imports it
+export const TASK_TAG_MAP: Record<string, string> = {};
+for (const { tag, label } of STATIC_TASK_OPTIONS) {
+  TASK_TAG_MAP[label] = tag;
+}
+export const TASK_NAMES = STATIC_TASK_OPTIONS.map(t => t.label);
+
+const PAGE_SIZE = 50;
+
+// ── Module-level caches (reused across component instances) ──
+const allModelsByTag: Map<string, HuggingFaceModelOption[]> = new Map();
+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();
+  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;
+
+  // ── Search state (client-side filtering over ALL models) ──
+  searchText = "";
+  private filteredModels: HuggingFaceModelOption[] | null = null;
+
+  private readonly destroy$ = new Subject<void>();
+  private subscription: Subscription | 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();
+    // 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.
+    setTimeout(() => this.syncTaskSelection(this.getCurrentTaskTag() ?? 
this.selectedTaskTag, false), 0);
+  }

Review Comment:
   This `setTimeout(..., 0)` handle is not tracked or cleared in `ngOnDestroy`. 
If the field is destroyed within the same tick (common when formly rebuilds the 
field tree on a task switch), the callback runs against a destroyed component 
and mutates form controls after teardown. Capture the id and `clearTimeout` it 
in `ngOnDestroy`.



##########
frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts:
##########
@@ -0,0 +1,543 @@
+/**
+ * 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 { 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" },
+];
+
+// Keep legacy export for any other code that imports it
+export const TASK_TAG_MAP: Record<string, string> = {};
+for (const { tag, label } of STATIC_TASK_OPTIONS) {
+  TASK_TAG_MAP[label] = tag;
+}
+export const TASK_NAMES = STATIC_TASK_OPTIONS.map(t => t.label);
+
+const PAGE_SIZE = 50;
+
+// ── Module-level caches (reused across component instances) ──
+const allModelsByTag: Map<string, HuggingFaceModelOption[]> = new Map();
+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();
+  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;
+
+  // ── Search state (client-side filtering over ALL models) ──
+  searchText = "";
+  private filteredModels: HuggingFaceModelOption[] | null = null;
+
+  private readonly destroy$ = new Subject<void>();
+  private subscription: Subscription | 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();
+    // 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.
+    setTimeout(() => this.syncTaskSelection(this.getCurrentTaskTag() ?? 
this.selectedTaskTag, false), 0);
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
+    this.subscription?.unsubscribe();
+  }
+
+  // ── 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)
+      const poll = setInterval(() => {
+        if (cachedTaskOptions !== null || tasksFetchError !== null) {
+          clearInterval(poll);
+          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$))
+      .subscribe({
+        next: tasks => {
+          tasksFetchSubscription = null;
+          cachedTaskOptions = tasks.length > 0 ? tasks : STATIC_TASK_OPTIONS;
+          this.taskOptions = cachedTaskOptions;
+          this.tasksLoading = false;
+          this.cdr.detectChanges();
+        },
+        error: (err: unknown) => {
+          console.error("Failed to load HuggingFace tasks:", err);
+          tasksFetchSubscription = null;
+          tasksFetchError = "Could not load tasks from Hugging Face. Using 
default list.";
+          this.tasksError = tasksFetchError;
+          this.taskOptions = STATIC_TASK_OPTIONS;
+          this.tasksLoading = false;
+          this.cdr.detectChanges();
+        },
+      });
+  }
+
+  retryTasksLoad(): void {
+    tasksFetchError = null;
+    this.tasksError = null;
+    this.loadTasks();
+  }
+
+  // ── Task selection ──
+
+  onTaskSelected(tag: string): void {
+    const previousTask = this.getCurrentTaskTag() ?? this.selectedTaskTag;
+    this.snapshotTaskState(previousTask);
+    this.syncTaskSelection(tag, true);
+    this.restoreTaskState(tag);
+    this.searchText = "";
+    this.filteredModels = null;
+    this.loadAllModels();
+  }
+
+  // ── Data loading ──
+
+  /**
+   * Fetch ALL models for the selected task.
+   * The backend paginates through HF Hub internally and caches the result.
+   * The first request per task may be slow; subsequent requests are instant.
+   */
+  private loadAllModels(): void {
+    const tag = this.selectedTaskTag || "text-generation";
+
+    this.loading = false;
+    this.errorMessage = null;
+
+    // Fast path: cached on the frontend
+    if (allModelsByTag.has(tag)) {
+      this.allModels = allModelsByTag.get(tag)!;
+      this.goToPage(0);
+      return;
+    }
+
+    // Previous error
+    if (errorByTag.has(tag)) {
+      this.errorMessage = errorByTag.get(tag)!;
+      this.allModels = [];
+      this.pagedModels = [];
+      this.totalPages = 0;
+      return;
+    }
+
+    // Cancel previous
+    this.subscription?.unsubscribe();
+    this.subscription = null;
+
+    this.allModels = [];
+    this.pagedModels = [];
+    this.totalPages = 0;
+
+    // Show spinner immediately for the initial fetch — it can take a while
+    // as the backend pages through HF Hub for the first time.
+    this.loading = true;
+    this.cdr.detectChanges();
+
+    this.subscription = this.http
+      .get<HuggingFaceModelOption[]>(
+        
`${AppSettings.getApiEndpoint()}/huggingface/models?task=${encodeURIComponent(tag)}`
+      )

Review Comment:
   The backend sets a truncation header when it stops paginating at 
`MAX_PAGES`, but `.get<T>()` drops response headers, so a truncated/partial 
model list is shown with no indication to the user. The backend also supports a 
`search` query param (server-side search), but the UI only filters the 
already-fetched list, so models beyond the fetched/truncated set are never 
searchable. Consider `{ observe: "response" }` to read the header and surface a 
"results may be incomplete" notice, and optionally wire search to the backend 
param.



-- 
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]

Reply via email to