diff --git a/web/src/lib/candidates.test.ts b/web/src/lib/candidates.test.ts index 3af4068..668dd2d 100644 --- a/web/src/lib/candidates.test.ts +++ b/web/src/lib/candidates.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { barWidth, candidateQuery, evidenceChips, funnelSteps, plural, scoreBarPercent } from './candidates'; +import { barWidth, candidateQuery, evidenceChips, funnelSteps, needsScoring, plural, scoreBarPercent } from './candidates'; import type { Candidate, Funnel } from './api'; const candidate = (over: Partial = {}): Candidate => ({ @@ -113,3 +113,14 @@ describe('plural', () => { expect(plural(12, 'candidate')).toBe('12 candidates'); }); }); + +describe('needsScoring', () => { + it('spots candidates analysed before a model existed', () => { + expect(needsScoring([candidate({ scored: false })])).toBe(true); + }); + + it('is quiet once everything carries a score', () => { + expect(needsScoring([candidate({ scored: true })])).toBe(false); + expect(needsScoring([])).toBe(false); + }); +}); diff --git a/web/src/lib/candidates.ts b/web/src/lib/candidates.ts index 8bc00ea..728417a 100644 --- a/web/src/lib/candidates.ts +++ b/web/src/lib/candidates.ts @@ -76,3 +76,6 @@ export function barWidth(value: number, total: number): number { } export const plural = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? '' : 's'}`; + +/** True when a case was analysed before a model existed: scoring can be run on its own. */ +export const needsScoring = (items: Candidate[]): boolean => items.some((c) => !c.scored); diff --git a/web/src/routes/cases/[id]/+page.svelte b/web/src/routes/cases/[id]/+page.svelte index 732dd0c..de61445 100644 --- a/web/src/routes/cases/[id]/+page.svelte +++ b/web/src/routes/cases/[id]/+page.svelte @@ -3,7 +3,7 @@ import { api, type Candidate, type CandidatePage, type Case, type Job, type VariantDetail } from '$lib/api'; import { poll } from '$lib/poll'; import { formatElapsed, latestStep } from '$lib/progress'; - import { plural } from '$lib/candidates'; + import { needsScoring, plural } from '$lib/candidates'; import CandidateRow from '$lib/components/CandidateRow.svelte'; import Funnel from '$lib/components/Funnel.svelte'; import VariantPanel from '$lib/components/VariantPanel.svelte'; @@ -31,6 +31,9 @@ const elapsedMs = $derived(job ? now - Date.parse(job.created_at) : 0); const step = $derived(scoring ? 'scoring variants with the model' : latestStep(job?.log ?? null)); const termCount = $derived(kase?.phenotypes.length ?? 0); + // Annotation is expensive and scoring is not: a case annotated before a model existed should + // not need a five-minute re-run of VEP to get its score. + const unscored = $derived(!!page && page.items.length > 0 && needsScoring(page.items)); // Tick the elapsed time while a run is in flight; polling refreshes the step itself. $effect(() => { @@ -154,6 +157,9 @@ {:else}

+ {#if unscored} + + {/if} {#if page && page.funnel.total > 0} Case report → {/if}