This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-7904-30681cd38406ca273f94714da2d7b118d8100f9f
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 797674477846b722cccd926b8928b3514b6a5993
Author: Prateek Ganigi <[email protected]>
AuthorDate: Wed Sep 2 18:14:14 2026 +0000

    fix(gui): only offer Hugging Face tasks the operator can run (#7904)
    
    ### What changes were proposed in this PR?
    
    The task dropdown offered six tasks the operator has no codegen for:
    `text-classification`, `token-classification`, `translation`,
    `summarization`, `feature-extraction`, and `fill-mask`. Selecting one
    silently fell back to text-generation and wrote raw JSON into the result
    column. The dynamic `/api/tasks` fetch could surface further unsupported
    tags on top of that. Meanwhile four tasks that *are* supported
    (`image-to-image`, `image-text-to-text`, `text-to-image`,
    `text-to-video`) were missing from the list.
    
    `STATIC_TASK_OPTIONS` is now exactly the 20 tasks with a registered
    codegen, grouped by the codegen that handles them, and a
    `SUPPORTED_TASK_TAGS` set derived from it filters the dynamic
    `/api/tasks` result. If that filter leaves nothing, say HF renames its
    tags, the static list is used rather than an empty dropdown. A comment
    ties the list to `HuggingFaceInferenceOpDesc.registeredCodegens` so the
    two stay in sync.
    
    ### Any related issues?
    
    Closes #7047
    
    ### How was this PR tested?
    
    95 tests pass in `hugging-face.component.spec.ts`, and Prettier is
    clean. Five tests were added: the unsupported tasks are absent from the
    list, the four newly added supported tasks are present,
    `SUPPORTED_TASK_TAGS` mirrors the static tags, an API response mixing
    supported and unsupported tasks is filtered down to the supported ones,
    and an all-unsupported response falls back to the static list.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Yes, this PR was co-authored with Claude in compliance with ASF policy.
---
 .../hugging-face/hugging-face.component.spec.ts    | 58 ++++++++++++++++++++++
 .../hugging-face/hugging-face.component.ts         | 44 ++++++++++++----
 2 files changed, 91 insertions(+), 11 deletions(-)

diff --git 
a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
 
b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
index dd199b8987..da53a0f20e 100644
--- 
a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
+++ 
b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.spec.ts
@@ -29,6 +29,7 @@ import {
   HuggingFaceModelOption,
   HuggingFaceTaskOption,
   STATIC_TASK_OPTIONS,
+  SUPPORTED_TASK_TAGS,
   invalidateHuggingFaceModelCache,
 } from "./hugging-face.component";
 
@@ -145,6 +146,32 @@ describe("HuggingFaceComponent (unit)", () => {
     const uniqueTags = new Set(tags);
     expect(uniqueTags.size).toBe(tags.length);
   });
+
+  it("should not list tasks that have no operator codegen", () => {
+    // These tasks were previously offered but have no TaskCodegen, so they
+    // silently fell back to text-generation and emitted raw JSON.
+    const unsupported = [
+      "text-classification",
+      "token-classification",
+      "translation",
+      "summarization",
+      "feature-extraction",
+      "fill-mask",
+    ];
+    const tags = STATIC_TASK_OPTIONS.map(t => t.tag);
+    unsupported.forEach(tag => expect(tags).not.toContain(tag));
+  });
+
+  it("should list the supported media/image tasks that have a codegen", () => {
+    const tags = STATIC_TASK_OPTIONS.map(t => t.tag);
+    ["image-to-image", "image-text-to-text", "text-to-image", 
"text-to-video"].forEach(tag =>
+      expect(tags).toContain(tag)
+    );
+  });
+
+  it("SUPPORTED_TASK_TAGS should exactly mirror the static task tags", () => {
+    expect([...SUPPORTED_TASK_TAGS].sort()).toEqual(STATIC_TASK_OPTIONS.map(t 
=> t.tag).sort());
+  });
 });
 
 // ── TestBed-based integration tests ──
@@ -252,6 +279,37 @@ describe("HuggingFaceComponent (TestBed)", () => {
       expect(component.tasksLoading).toBe(false);
     });
 
+    it("should drop unsupported tasks from the dynamic API response", () => {
+      const { field } = buildFieldWithFormGroup();
+      component.field = field;
+      fixture.detectChanges();
+
+      // HF returns a mix of supported and unsupported tasks.
+      http.expectOne(`${API}/huggingface/tasks`).flush([
+        { tag: "text-generation", label: "Text Generation" },
+        { tag: "summarization", label: "Summarization" }, // no codegen
+        { tag: "image-classification", label: "Image Classification" },
+        { tag: "fill-mask", label: "Fill-Mask" }, // no codegen
+      ]);
+      http.expectOne(req => 
req.url.startsWith(`${API}/huggingface/models`)).flush([]);
+
+      expect(component.taskOptions.map(t => 
t.tag)).toEqual(["text-generation", "image-classification"]);
+    });
+
+    it("should fall back to STATIC_TASK_OPTIONS when the API returns only 
unsupported tasks", () => {
+      const { field } = buildFieldWithFormGroup();
+      component.field = field;
+      fixture.detectChanges();
+
+      http.expectOne(`${API}/huggingface/tasks`).flush([
+        { tag: "translation", label: "Translation" },
+        { tag: "fill-mask", label: "Fill-Mask" },
+      ]);
+      http.expectOne(req => 
req.url.startsWith(`${API}/huggingface/models`)).flush([]);
+
+      expect(component.taskOptions).toEqual(STATIC_TASK_OPTIONS);
+    });
+
     it("retryTasksLoad should clear error and re-fetch tasks", fakeAsync(() => 
{
       initComponent();
 
diff --git 
a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts 
b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts
index f634c66a2a..4925656425 100644
--- 
a/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts
+++ 
b/frontend/src/app/workspace/component/hugging-face/hugging-face.component.ts
@@ -44,32 +44,49 @@ export interface HuggingFaceTaskOption {
   label: string;
 }
 
-// ── Static fallback task list (used when the dynamic fetch fails) ──
+// ── Supported task list ──
+// The tasks the operator actually has a codegen for. Anything outside this set
+// has no codegen and would silently fall back to text-generation (emitting raw
+// JSON), so the UI must not offer it. This doubles as the static fallback list
+// shown when the dynamic /api/tasks fetch fails.
+//
+// Keep in sync with the codegens registered in
+// `HuggingFaceInferenceOpDesc.registeredCodegens` (TextGen / ImageTask /
+// AudioTask / MediaGen / QaRanking). A newly added codegen surfaces in the UI
+// only once its task tag is added here.
 export const STATIC_TASK_OPTIONS: HuggingFaceTaskOption[] = [
+  // text-generation (TextGenCodegen)
   { 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" },
+  // question-answering family (QaRankingCodegen)
   { 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" },
+  // audio (AudioTaskCodegen)
+  { tag: "automatic-speech-recognition", label: "Automatic Speech Recognition" 
},
+  { tag: "audio-classification", label: "Audio Classification" },
+  { tag: "text-to-speech", label: "Text to Speech" },
+  // image (ImageTaskCodegen)
   { 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: "image-to-image", label: "Image to Image" },
+  { tag: "image-text-to-text", label: "Image-Text 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" },
+  // media generation (MediaGenCodegen)
+  { tag: "text-to-image", label: "Text to Image" },
+  { tag: "text-to-video", label: "Text to Video" },
 ];
 
+// Task tags the operator supports — the single gate for what the UI may offer.
+// Derived from STATIC_TASK_OPTIONS so the two never drift; used to filter the
+// dynamic /api/tasks result down to tasks that actually have a codegen.
+export const SUPPORTED_TASK_TAGS: ReadonlySet<string> = new 
Set(STATIC_TASK_OPTIONS.map(o => o.tag));
+
 const PAGE_SIZE = 50;
 
 const TRUNCATED_HEADER = "X-Texera-Truncated";
@@ -262,7 +279,12 @@ export class HuggingFaceComponent extends 
FieldType<FieldTypeConfig> implements
       .subscribe({
         next: tasks => {
           tasksFetchSubscription = null;
-          cachedTaskOptions = tasks.length > 0 ? tasks : STATIC_TASK_OPTIONS;
+          // Only surface tasks the operator has a codegen for — anything else
+          // silently falls back to text-generation and emits raw JSON. If the
+          // filter leaves nothing (e.g. HF changed its tags), use the static
+          // supported list rather than showing an empty dropdown.
+          const supported = tasks.filter(t => SUPPORTED_TASK_TAGS.has(t.tag));
+          cachedTaskOptions = supported.length > 0 ? supported : 
STATIC_TASK_OPTIONS;
           this.taskOptions = cachedTaskOptions;
           this.tasksLoading = false;
           this.cdr.detectChanges();

Reply via email to