Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to the Fovea project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.3] - 2026-06-24

The 0.5.3 patch fixes a claims-workspace interaction bug ([#177](https://github.com/parafovea/fovea/pull/177)). No API shapes change and nothing is breaking.

### Fixed

#### Clicking a Claim Card No Longer Switches to the Summary Tab

- In the video summary editor's Claims tab, clicking a claim card switched the interface back to the Summary tab, which is not what clicking a card should do. A card now selects in place: it is marked selected on the Claims tab and records its source spans so the Summary tab highlights the claim's provenance only if the user chooses to switch there. The card's selected styling, previously bound to a state value that was never set, is now driven by the selected-claim state (`annotation-tool/src/components/video/VideoSummaryEditor.tsx`, `annotation-tool/src/components/claims/ClaimsViewer.tsx`).

## [0.5.2] - 2026-06-22

The 0.5.2 patch fixes a Safari-only failure ([#143](https://github.com/parafovea/fovea/issues/143)) where a video in the annotation workspace blacked out the moment it was paused and jumped position on resume, while Chrome and Firefox played it back correctly. The cause was twofold: the video stream endpoint mishandled the byte-range requests Safari issues but Chrome does not, and WebKit dropped the paused video frame from its compositor. No API shapes change and nothing is breaking.
Expand Down
2 changes: 1 addition & 1 deletion annotation-tool/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fovea/annotation-tool",
"private": true,
"version": "0.5.2",
"version": "0.5.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 2 additions & 0 deletions annotation-tool/src/components/claims/ClaimsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ const ClaimTreeNode = memo(function ClaimTreeNode({
return (
<div style={{ marginLeft: `${depth * 1.5}rem` }}>
<div
data-testid="claim-card"
data-selected={isSelected ? 'true' : 'false'}
className={cn(
'mb-2 cursor-pointer rounded-lg border p-3 transition-colors duration-200',
isSelected ? 'bg-accent' : 'bg-card hover:bg-accent/50',
Expand Down
16 changes: 7 additions & 9 deletions annotation-tool/src/components/video/VideoSummaryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ const VideoSummaryEditor = forwardRef<VideoSummaryEditorRef, VideoSummaryEditorP
const modelsDisabled = !modelConfig?.cudaAvailable && !modelConfig?.cpuModelsAvailable

// Claims UI state from Zustand
const selectedClaimId = useClaimsUiStore((state) => state.selectedClaimId)
const extracting = useClaimsUiStore((state) => state.extracting)
const extractionJobId = useClaimsUiStore((state) => state.extractionJobId)
const extractionProgress = useClaimsUiStore((state) => state.extractionProgress)
Expand Down Expand Up @@ -399,16 +398,15 @@ const VideoSummaryEditor = forwardRef<VideoSummaryEditorRef, VideoSummaryEditorP

const handleTabChange = (value: string) => {
setActiveTab(value)
// Clear highlighting when switching tabs
if (value === 'summary') {
setHighlightedSpans([])
setHighlightedClaimId(null)
}
}

const handleClaimSelect = (claimId: string, sourceSpans: ClaimTextSpan[]) => {
// Switch to Summary tab to show highlighted text
setActiveTab('summary')
// Select the claim in place on the Claims tab; do NOT navigate away.
// Selecting a card marks it as selected and records its source spans so
// the Summary tab highlights the claim's provenance if and when the user
// chooses to switch there (dismissed with the "×" on that view). Clicking
// a card previously yanked the user to the Summary tab, which is not what
// clicking a card should do.
setHighlightedSpans(sourceSpans)
setHighlightedClaimId(claimId)
}
Expand Down Expand Up @@ -626,7 +624,7 @@ const VideoSummaryEditor = forwardRef<VideoSummaryEditorRef, VideoSummaryEditorP
onEditClaim={handleEditClaim}
onAddClaim={handleAddClaim}
onDeleteClaim={handleDeleteClaim}
selectedClaimId={selectedClaimId}
selectedClaimId={highlightedClaimId}
onClaimSelect={handleClaimSelect}
loading={claimsLoading}
error={claimsError ? (claimsError instanceof Error ? claimsError.message : String(claimsError)) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @file claim-card-selection.spec.ts
* @description E2E coverage for clicking a claim card in the claims viewer.
* Clicking a card selects it in place and must keep the user on the Claims
* tab; it must not navigate back to the Summary tab.
*/

import { test, expect } from '../../fixtures/test-context.js'

const API = 'http://localhost:3001'

test.describe('Claim card selection', () => {
/**
* Seed a summary with a single leaf claim, open the Claims tab, click the
* claim card, and assert the click selects the card and leaves the user on
* the Claims tab rather than switching to the Summary tab.
*/
test('clicking a claim card selects it and stays on the Claims tab', async ({
page,
testVideo,
testPersona,
workerSessionToken,
annotationWorkspace,
}) => {
const cookie = `session_token=${workerSessionToken}`

// Seed a summary for (video, persona) with source text the claim spans.
const summaryRes = await fetch(`${API}/api/summaries`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: cookie },
body: JSON.stringify({
videoId: testVideo.id,
personaId: testPersona.id,
summary: [{ type: 'text', content: 'A red car drives through the intersection.' }],
}),
})
expect(summaryRes.ok).toBe(true)
const summary = await summaryRes.json()

// Seed a single leaf claim (no subclaims), carrying source text spans so
// the old behavior would have shown the Summary tab's highlight view.
const claimText = `Card-select claim ${Date.now()}`
const claimRes = await fetch(`${API}/api/summaries/${summary.id}/claims`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: cookie },
body: JSON.stringify({
summaryType: 'video',
text: claimText,
audio: ['speech'],
textSpans: [{ charStart: 0, charEnd: 9 }],
}),
})
expect(claimRes.ok).toBe(true)

// Open the summary dialog, pick the persona, switch to the Claims tab.
await annotationWorkspace.navigateTo(testVideo.id)
await page.waitForSelector('[data-testid="video-player"], video', { timeout: 10000 })
await page.getByRole('button', { name: /edit summary/i }).click()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
const personaSelect = dialog.getByLabel(/select persona/i)
if (await personaSelect.isVisible()) {
await personaSelect.click()
await page
.getByRole('option')
.filter({ hasText: new RegExp('^' + testPersona.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ' \\(') })
.first()
.click()
await page.waitForTimeout(500)
}

const summaryTab = dialog.locator('[role="tab"]').filter({ hasText: 'Summary' })
const claimsTab = dialog.locator('[role="tab"]').filter({ hasText: 'Claims' })
await claimsTab.click()

// Precondition: the claim card is shown, unselected, on the Claims tab.
await expect(page.getByText(claimText)).toBeVisible({ timeout: 10000 })
const card = dialog.getByTestId('claim-card')
await expect(card).toHaveAttribute('data-selected', 'false')

// Click the card body (the claim text, not an action button).
await page.getByText(claimText).click()

// The card is now selected, and we are still on the Claims tab — the
// claims viewer stays visible, the Claims tab stays active, and the
// Summary tab's highlight view never takes over.
await expect(card).toHaveAttribute('data-selected', 'true')
await expect(dialog.locator('[data-tour-id="claims-viewer"]')).toBeVisible()
await expect(claimsTab).toHaveAttribute('aria-selected', 'true')
await expect(summaryTab).toHaveAttribute('aria-selected', 'false')
await expect(
page.getByText(/showing highlighted text for selected claim/i)
).toBeHidden()
})
})
10 changes: 10 additions & 0 deletions docs/docs/project/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ All notable changes to the Fovea project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.3] - 2026-06-24

The 0.5.3 patch fixes a claims-workspace interaction bug ([#177](https://github.com/parafovea/fovea/pull/177)). No API shapes change and nothing is breaking.

### Fixed

#### Clicking a Claim Card No Longer Switches to the Summary Tab

- In the video summary editor's Claims tab, clicking a claim card switched the interface back to the Summary tab, which is not what clicking a card should do. A card now selects in place: it is marked selected on the Claims tab and records its source spans so the Summary tab highlights the claim's provenance only if the user chooses to switch there. The card's selected styling, previously bound to a state value that was never set, is now driven by the selected-claim state (`annotation-tool/src/components/video/VideoSummaryEditor.tsx`, `annotation-tool/src/components/claims/ClaimsViewer.tsx`).

## [0.5.2] - 2026-06-22

The 0.5.2 patch fixes a Safari-only failure ([#143](https://github.com/parafovea/fovea/issues/143)) where a video in the annotation workspace blacked out the moment it was paused and jumped position on resume, while Chrome and Firefox played it back correctly. The cause was twofold: the video stream endpoint mishandled the byte-range requests Safari issues but Chrome does not, and WebKit dropped the paused video frame from its compositor. No API shapes change and nothing is breaking.
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "docs",
"version": "0.5.2",
"version": "0.5.3",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
Expand Down
2 changes: 1 addition & 1 deletion model-service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fovea-model-service",
"version": "0.5.2",
"version": "0.5.3",
"description": "AI model inference service for video annotation",
"scripts": {
"dev": "source venv/bin/activate && uvicorn src.main:app --reload --host 0.0.0.0 --port 8000",
Expand Down
2 changes: 1 addition & 1 deletion model-service/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "fovea-model-service"
version = "0.5.2"
version = "0.5.3"
description = "Model service for fovea video annotation tool"
requires-python = ">=3.12"
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fovea",
"private": true,
"version": "0.5.2",
"version": "0.5.3",
"description": "FOVEA - Flexible Ontology Visual Event Analyzer",
"packageManager": "pnpm@10.15.0",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fovea/server",
"version": "0.5.2",
"version": "0.5.3",
"private": true,
"type": "module",
"scripts": {
Expand Down
Loading