Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
046734f
feat(frontend): add HuggingFace task selector and model browser compo…
ELin2025 May 29, 2026
f933fbe
fix(frontend): add explicit type annotations to rxjs error callbacks
ELin2025 Jun 8, 2026
1f5a0f4
style(frontend): format HuggingFace component with prettier
ELin2025 Jun 8, 2026
11e7cb9
fix(frontend): add takeUntil to rxjs subscribe calls in HuggingFaceCo…
ELin2025 Jun 8, 2026
6742c3c
fix(frontend): fix memory leaks and remove dead code in HuggingFaceCo…
ELin2025 Jun 11, 2026
41c9179
style(frontend): format HuggingFaceComponent with prettier
ELin2025 Jun 11, 2026
7ce8500
fix(frontend): add in-flight dedup guard for model fetches in Hugging…
ELin2025 Jun 11, 2026
f383eed
feat(frontend): surface truncation notice and wire server-side search…
ELin2025 Jun 11, 2026
66edef8
fix(frontend): restore takeUntil on subscribes to satisfy rxjs-angula…
ELin2025 Jun 11, 2026
95b8e4b
fix(frontend): add finalize guard to model fetch for cancellation cle…
ELin2025 Jun 16, 2026
4284d6d
Potential fix for pull request finding
ELin2025 Jun 20, 2026
91b5dde
fix(frontend): address Copilot review feedback on HuggingFaceComponent
ELin2025 Jun 20, 2026
0223dc5
fix(frontend): handle search stream errors and fix clear model value
ELin2025 Jun 20, 2026
cf6c20f
style(frontend): format HuggingFaceComponent with prettier
ELin2025 Jun 20, 2026
a784f3d
test(frontend): add comprehensive TestBed tests for HuggingFaceComponent
ELin2025 Jun 23, 2026
b03abf2
feat(frontend): add HuggingFace audio upload component
ELin2025 May 29, 2026
d4eac94
style(frontend): format audio upload spec with prettier
ELin2025 Jun 8, 2026
e999d94
style(frontend): format audio upload HTML with prettier
ELin2025 Jun 11, 2026
e2a2dfb
fix(frontend): guard concurrent uploads and remove dead test in audio…
ELin2025 Jun 11, 2026
33c5d7a
test(frontend): add TestBed-based tests for HuggingFaceAudioUploadCom…
ELin2025 Jun 12, 2026
35d33eb
fix(frontend): guard against stale upload response after clear
ELin2025 Jun 20, 2026
c900e01
fix(frontend): disable file input and clear button during audio upload
ELin2025 Jun 20, 2026
59fe00d
test(frontend): add comprehensive tests for HuggingFaceAudioUploadCom…
ELin2025 Jun 23, 2026
a1d62ba
feat(frontend): add task-aware field visibility and preview to Huggin…
ELin2025 May 29, 2026
739a2c4
test(frontend): add HuggingFace visibility tests and mock operator data
ELin2025 May 29, 2026
9348b18
style(frontend): format spec files with prettier
ELin2025 Jun 8, 2026
01fae10
style(frontend): format property editor component with prettier
ELin2025 Jun 11, 2026
e1c16d1
fix(frontend): fix HuggingFace field visibility and add missing previ…
ELin2025 Jun 12, 2026
373d992
fix(frontend): add missing preview assets and guard deleted operator
ELin2025 Jun 20, 2026
f789c4b
fix(huggingface): use CANDIDATE_LABELS property for zero-shot image c…
ELin2025 Jun 20, 2026
caa6fdc
test(frontend): add HuggingFace field visibility and validator tests
ELin2025 Jun 20, 2026
3094d1e
test(frontend): add HuggingFace field visibility, validator, and prev…
ELin2025 Jun 23, 2026
a22b2f9
fix(auth): handle missing JWT claims
anishshiva7 Jun 19, 2026
352f9cf
feat(huggingFace): add image task family via ImageTaskCodegen
PG1204 May 28, 2026
6fbc4e7
chore: retrigger CI
PG1204 Jun 16, 2026
a0c1bb7
chore: retrigger CI
PG1204 Jun 16, 2026
ae2c738
chore: retrigger CI
PG1204 Jun 16, 2026
2a00c4a
feat(huggingFace): add HuggingFaceModelResource for model browsing an…
PG1204 May 17, 2026
66ada29
fix: address review feedback on HuggingFaceModelResource
PG1204 May 19, 2026
e451064
fix(huggingFace): cap heap usage on /media-proxy and /audio-preview
PG1204 May 28, 2026
a04f4e7
chore: retrigger CI
PG1204 May 28, 2026
0f5791c
chore(huggingFace): annotate HuggingFaceModelResource with @RolesAllowed
PG1204 May 29, 2026
b86ad62
style(frontend): format HuggingFace components with prettier
ELin2025 Jun 8, 2026
3a0a455
feat(frontend): render HuggingFace media results inline in the result…
juliethecao Jun 13, 2026
9fd8db5
fix(frontend): use toBe(true/false) instead of toBeTrue/toBeFalse in …
juliethecao Jun 13, 2026
9a5664b
fix(frontend): use observe: "response" to access HuggingFace response…
juliethecao Jun 22, 2026
b5ec846
fix(frontend): decouple shared HuggingFace tasks fetch from component…
juliethecao Jun 22, 2026
6fa2a49
fix(frontend): guard against JSON.stringify returning undefined in re…
juliethecao Jun 22, 2026
306aab6
fix(frontend): remove unused NgxJsonViewerModule, guard JSON.stringif…
juliethecao Jun 22, 2026
0e8fc58
style(huggingFace): apply scalafmt formatting
juliethecao Jun 22, 2026
d6415b3
style(frontend): apply prettier formatting to result-panel-modal
juliethecao Jun 22, 2026
82163e7
fix(frontend): satisfy rxjs-angular/prefer-takeuntil lint rule in Hug…
juliethecao Jun 22, 2026
5634a94
fix(frontend): fetch remote HuggingFace media via HttpClient to send …
juliethecao Jun 29, 2026
577eadb
test(frontend): increase patch coverage for HuggingFace and result-pa…
juliethecao Jun 29, 2026
4f678b6
Merge branch 'main' into hf/08-result-media
juliethecao Jun 29, 2026
ccff397
style(frontend): format result-panel-modal files with prettier
juliethecao Jun 29, 2026
81c371f
Merge branch 'hf/08-result-media' of https://github.com/ELin2025/texe…
juliethecao Jun 29, 2026
aa90b9c
Merge branch 'main' into hf/08-result-media
juliethecao Jun 30, 2026
fcdd816
test(frontend): fix merge collision syntax errors and add template co…
juliethecao Jun 30, 2026
e29bc6a
Merge remote-tracking branch 'origin/main' into hf/08-result-media
juliethecao Jul 7, 2026
41239a2
test(frontend): add HuggingFace task preview template coverage for op…
juliethecao Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import java.util.Optional
object UserAuthenticator extends Authenticator[JwtContext, SessionUser] with LazyLogging {
override def authenticate(context: JwtContext): Optional[SessionUser] = {
try {
Optional.of(JwtParser.claimsToSessionUser(context.getJwtClaims))
JwtParser.claimsToOptionalSessionUser(context.getJwtClaims)
} catch {
case e: Exception =>
logger.error("Failed to authenticate the JwtContext", e)
Expand Down
23 changes: 20 additions & 3 deletions common/auth/src/main/scala/org/apache/texera/auth/JwtParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object JwtParser extends LazyLogging {
/** Verify and parse a Bearer token string. */
def parseToken(token: String): Optional[SessionUser] = {
try {
Optional.of(claimsToSessionUser(JwtAuth.jwtConsumer.processToClaims(token)))
claimsToOptionalSessionUser(JwtAuth.jwtConsumer.processToClaims(token))
} catch {
case _: UnresolvableKeyException =>
logger.error("Invalid JWT Signature")
Expand All @@ -49,6 +49,19 @@ object JwtParser extends LazyLogging {
}
}

/** Convert already-verified claims to a [[SessionUser]], returning empty when
* the required Texera custom claims are missing or malformed.
*/
def claimsToOptionalSessionUser(claims: JwtClaims): Optional[SessionUser] = {
try {
Optional.of(claimsToSessionUser(claims))
} catch {
case e: IllegalArgumentException =>
logger.error(s"Invalid JWT claims: ${e.getMessage}")
Optional.empty()
}
}

/** Build a [[SessionUser]] from already-verified claims. Used by both
* [[parseToken]] (which verifies then calls this) and amber's
* `UserAuthenticator` (which the toastshaman filter calls after its own
Expand All @@ -59,8 +72,12 @@ object JwtParser extends LazyLogging {
val email = claims.getClaimValue("email", classOf[String])
// jose4j returns Long after JSON round-trip but the original setClaim
// call writes Integer; widen via Number to handle both cases.
val userId = claims.getClaimValue("userId", classOf[Number]).intValue()
val role = UserRoleEnum.valueOf(claims.getClaimValue("role").asInstanceOf[String])
val userId = Option(claims.getClaimValue("userId", classOf[Number]))
.map(_.intValue())
.getOrElse(throw new IllegalArgumentException("JWT claim 'userId' is required."))
val roleName = Option(claims.getClaimValue("role", classOf[String]))
.getOrElse(throw new IllegalArgumentException("JWT claim 'role' is required."))
val role = UserRoleEnum.valueOf(roleName)
val googleId = claims.getClaimValue("googleId", classOf[String])
val googleAvatar = claims.getClaimValue("googleAvatar", classOf[String])
val user = new User(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ class JwtParserSpec extends AnyFlatSpec with Matchers {
u.getGoogleAvatar shouldBe "avatar-blob"
}

it should "return empty when already-verified claims are missing userId" in {
val claims = buildClaims()
claims.unsetClaim("userId")
JwtParser.claimsToOptionalSessionUser(claims).isPresent shouldBe false
}

it should "return empty when already-verified claims are missing role" in {
val claims = buildClaims()
claims.unsetClaim("role")
JwtParser.claimsToOptionalSessionUser(claims).isPresent shouldBe false
}

"JwtParser.parseToken" should "return empty on a structurally invalid token" in {
JwtParser.parseToken("not-a-real-jwt").isPresent shouldBe false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,16 @@ object ImageTaskCodegen extends TaskCodegen {
| use_raw_binary_body = True
| raw_binary_headers = image_headers
| elif task == "zero-shot-image-classification":
| # Zero-shot requires the caller to supply candidate labels.
| # We reuse the prompt column as a comma-separated label list so
| # the task is shippable without a dedicated operator field.
| # TODO: replace with a first-class `candidateLabels` field once
| # the property panel supports task-specific inputs.
| #
| # Fail fast if usable labels can't be derived. Both modes lead to
| # a meaningless inference call:
| # 1. Empty prompt column -> labels = []
| # The HF API rejects candidate_labels: [] with an opaque 400.
| # 2. Missing prompt column -> upstream sets prompt_value
| # to the fallback "What is shown in this image?", which has
| # no comma, so labels collapses to a single nonsense entry.
| # Zero-shot classification needs >= 2 candidate labels to be
| # meaningful — surface a configuration error in both cases.
| labels = [s.strip() for s in prompt_value.split(",") if s.strip()]
| # Prefer the dedicated candidateLabels property; fall back to
| # the prompt column for backward compatibility.
| label_source = (self.CANDIDATE_LABELS or "").strip() if self.CANDIDATE_LABELS else ""
| if not label_source and prompt_value:
| label_source = prompt_value
| labels = [s.strip() for s in label_source.split(",") if s.strip()]
| if len(labels) < 2:
| raise ValueError(
| "zero-shot-image-classification requires at least 2 candidate "
| "labels: provide a comma-separated list in the prompt column."
| "labels: provide a comma-separated list in the Candidate Labels field."
| )
| payload = {
| "inputs": self._image_input_as_base64(current_image_bytes),
Expand Down
148 changes: 148 additions & 0 deletions frontend/src/app/common/util/media-type.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* 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 { isAudioUrl, isImageUrl, isVideoUrl } from "./media-type.util";

describe("isImageUrl", () => {
it("should return true for data:image/ data URLs", () => {
expect(isImageUrl("data:image/png;base64,abc123")).toBe(true);
expect(isImageUrl("data:image/jpeg;base64,abc123")).toBe(true);
expect(isImageUrl("data:image/webp;base64,abc123")).toBe(true);
});

it("should return true for common image file extensions", () => {
expect(isImageUrl("https://example.com/photo.png")).toBe(true);
expect(isImageUrl("https://example.com/photo.jpg")).toBe(true);
expect(isImageUrl("https://example.com/photo.jpeg")).toBe(true);
expect(isImageUrl("https://example.com/photo.gif")).toBe(true);
expect(isImageUrl("https://example.com/photo.webp")).toBe(true);
});

it("should be case-insensitive for extensions", () => {
expect(isImageUrl("https://example.com/photo.PNG")).toBe(true);
expect(isImageUrl("https://example.com/photo.JPG")).toBe(true);
});

it("should return true for URLs with query strings", () => {
expect(isImageUrl("https://example.com/photo.png?v=1")).toBe(true);
});

it("should return false for audio and video URLs", () => {
expect(isImageUrl("data:audio/mp3;base64,abc")).toBe(false);
expect(isImageUrl("data:video/mp4;base64,abc")).toBe(false);
expect(isImageUrl("https://example.com/clip.mp4")).toBe(false);
});

it("should return false for plain text strings", () => {
expect(isImageUrl("hello world")).toBe(false);
expect(isImageUrl("")).toBe(false);
});
});

describe("isAudioUrl", () => {
it("should return true for data:audio/ data URLs", () => {
expect(isAudioUrl("data:audio/mp3;base64,abc123")).toBe(true);
expect(isAudioUrl("data:audio/wav;base64,abc123")).toBe(true);
});

it("should return true for common audio file extensions", () => {
expect(isAudioUrl("https://example.com/clip.mp3")).toBe(true);
expect(isAudioUrl("https://example.com/clip.wav")).toBe(true);
expect(isAudioUrl("https://example.com/clip.ogg")).toBe(true);
expect(isAudioUrl("https://example.com/clip.m4a")).toBe(true);
expect(isAudioUrl("https://example.com/clip.flac")).toBe(true);
});

it("should be case-insensitive for extensions", () => {
expect(isAudioUrl("https://example.com/clip.MP3")).toBe(true);
expect(isAudioUrl("https://example.com/clip.WAV")).toBe(true);
});

it("should return true for URLs with query strings", () => {
expect(isAudioUrl("https://example.com/clip.mp3?token=xyz")).toBe(true);
});

it("should return false for image and video URLs", () => {
expect(isAudioUrl("data:image/png;base64,abc")).toBe(false);
expect(isAudioUrl("data:video/mp4;base64,abc")).toBe(false);
expect(isAudioUrl("https://example.com/photo.png")).toBe(false);
});

it("should return false for plain text strings", () => {
expect(isAudioUrl("hello world")).toBe(false);
expect(isAudioUrl("")).toBe(false);
});
});

describe("isVideoUrl", () => {
it("should return true for data:video/ data URLs", () => {
expect(isVideoUrl("data:video/mp4;base64,abc123")).toBe(true);
expect(isVideoUrl("data:video/webm;base64,abc123")).toBe(true);
});

it("should return true for common video file extensions", () => {
expect(isVideoUrl("https://example.com/clip.mp4")).toBe(true);
expect(isVideoUrl("https://example.com/clip.webm")).toBe(true);
expect(isVideoUrl("https://example.com/clip.ogv")).toBe(true);
});

it("should return true for fal.media CDN URLs", () => {
expect(isVideoUrl("https://v3b.fal.media/files/abc123/output.mp4")).toBe(true);
});

it("should be case-insensitive for extensions", () => {
expect(isVideoUrl("https://example.com/clip.MP4")).toBe(true);
expect(isVideoUrl("https://example.com/clip.WEBM")).toBe(true);
});

it("should return true for URLs with query strings", () => {
expect(isVideoUrl("https://example.com/clip.mp4?t=5")).toBe(true);
});

it("should return false for image and audio URLs", () => {
expect(isVideoUrl("data:image/png;base64,abc")).toBe(false);
expect(isVideoUrl("data:audio/mp3;base64,abc")).toBe(false);
expect(isVideoUrl("https://example.com/photo.jpg")).toBe(false);
});

it("should return false for plain text strings", () => {
expect(isVideoUrl("hello world")).toBe(false);
expect(isVideoUrl("")).toBe(false);
});

it("should return false for non-string types", () => {
expect(isVideoUrl(null as unknown as string)).toBe(false);
expect(isVideoUrl(undefined as unknown as string)).toBe(false);
expect(isVideoUrl(42 as unknown as string)).toBe(false);
});
});

describe("non-string type guard (shared)", () => {
it("isAudioUrl should return false for non-string types", () => {
expect(isAudioUrl(null as unknown as string)).toBe(false);
expect(isAudioUrl(undefined as unknown as string)).toBe(false);
expect(isAudioUrl(true as unknown as string)).toBe(false);
});

it("isImageUrl should return false for non-string types", () => {
expect(isImageUrl(null as unknown as string)).toBe(false);
expect(isImageUrl(undefined as unknown as string)).toBe(false);
expect(isImageUrl([] as unknown as string)).toBe(false);
});
});
37 changes: 37 additions & 0 deletions frontend/src/app/common/util/media-type.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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.
*/

export function isVideoUrl(value: string): boolean {
if (typeof value !== "string") return false;
return (
value.match(/\.(mp4|webm|ogv)(\?.*)?$/i) !== null ||
value.startsWith("data:video/") ||
value.startsWith("https://v3b.fal.media/files/")
);
}

export function isAudioUrl(value: string): boolean {
if (typeof value !== "string") return false;
return value.match(/\.(mp3|wav|ogg|m4a|flac)(\?.*)?$/i) !== null || value.startsWith("data:audio/");
}

export function isImageUrl(value: string): boolean {
if (typeof value !== "string") return false;
return value.match(/\.(png|jpg|jpeg|gif|webp)(\?.*)?$/i) !== null || value.startsWith("data:image/");
}
Loading
Loading