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 { 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';
|
import type { Candidate, Funnel } from './api';
|
||||||
|
|
||||||
const candidate = (over: Partial<Candidate> = {}): Candidate => ({
|
const candidate = (over: Partial<Candidate> = {}): Candidate => ({
|
||||||
@@ -113,3 +113,14 @@ describe('plural', () => {
|
|||||||
expect(plural(12, 'candidate')).toBe('12 candidates');
|
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'}`;
|
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 { api, type Candidate, type CandidatePage, type Case, type Job, type VariantDetail } from '$lib/api';
|
||||||
import { poll } from '$lib/poll';
|
import { poll } from '$lib/poll';
|
||||||
import { formatElapsed, latestStep } from '$lib/progress';
|
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 CandidateRow from '$lib/components/CandidateRow.svelte';
|
||||||
import Funnel from '$lib/components/Funnel.svelte';
|
import Funnel from '$lib/components/Funnel.svelte';
|
||||||
import VariantPanel from '$lib/components/VariantPanel.svelte';
|
import VariantPanel from '$lib/components/VariantPanel.svelte';
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
const elapsedMs = $derived(job ? now - Date.parse(job.created_at) : 0);
|
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 step = $derived(scoring ? 'scoring variants with the model' : latestStep(job?.log ?? null));
|
||||||
const termCount = $derived(kase?.phenotypes.length ?? 0);
|
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.
|
// Tick the elapsed time while a run is in flight; polling refreshes the step itself.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -154,6 +157,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<button onclick={analyse} disabled={busy}>{job ? 'Re-analyse case' : 'Analyse case'}</button>
|
<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}
|
{#if page && page.funnel.total > 0}
|
||||||
<a class="reportlink" href="/cases/{data.caseId}/report">Case report →</a>
|
<a class="reportlink" href="/cases/{data.caseId}/report">Case report →</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user