feat(web): score an already-annotated case without re-running the pipeline
A case analysed before a model existed stayed unscored forever: the only control on the page was "Re-analyse case", which re-runs five minutes of VEP to obtain a score that takes a second. When any candidate is unscored the page now offers "Score variants" on its own. Tests: web 34; svelte-check clean.
This commit is contained in:
@@ -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> = {}): 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
<p class="actions">
|
||||
<button onclick={analyse} disabled={busy}>{job ? 'Re-analyse case' : 'Analyse case'}</button>
|
||||
{#if unscored}
|
||||
<button class="quiet" onclick={scoreThenLoad}>Score variants</button>
|
||||
{/if}
|
||||
{#if page && page.funnel.total > 0}
|
||||
<a class="reportlink" href="/cases/{data.caseId}/report">Case report →</a>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user