feature: Self-Service Clinical Testing Sessions
CI / backend (push) Successful in 8m52s
CI / frontend (push) Failing after 1m39s

This commit is contained in:
voltsrage
2026-08-06 04:03:04 +08:00
parent 1d28880920
commit 80b009fd23
41 changed files with 4922 additions and 123 deletions
@@ -1,7 +1,8 @@
<script setup>
import { onMounted } from 'vue'
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useAlertQualityStore } from '@/stores/alertQuality'
import { useSimulationStore } from '@/stores/simulation'
import { alertTypeLabel } from '@/api/normalize'
import Card from '@/components/ui/Card.vue'
import Badge from '@/components/ui/Badge.vue'
@@ -10,7 +11,20 @@ import Skeleton from '@/components/ui/Skeleton.vue'
import AlertQualityChart from '@/components/charts/AlertQualityChart.vue'
const store = useAlertQualityStore()
const { summaryStats, byAlertType, snapshots, loading, error, periodDays } = storeToRefs(store)
const simulationStore = useSimulationStore()
const {
summaryStats,
byAlertType,
byScenario,
scenarioOptions,
snapshots,
feedbackRows,
loading,
error,
periodDays,
selectedScenarioId,
} = storeToRefs(store)
const { enabled: simulationEnabled } = storeToRefs(simulationStore)
const periodOptions = [
{ label: '7 days', value: 7 },
@@ -18,12 +32,32 @@ const periodOptions = [
{ label: '30 days', value: 30 },
]
onMounted(() => store.loadDashboard())
const showScenarioAttribution = computed(() =>
simulationEnabled.value
|| feedbackRows.value.some(r => Object.prototype.hasOwnProperty.call(r, 'scenarioId')),
)
onMounted(async () => {
if (!simulationStore.enabled) {
await simulationStore.loadConfig()
}
await store.loadDashboard()
})
function changePeriod(days) {
store.setPeriodDays(days)
store.loadDashboard()
}
function changeScenario(event) {
const value = event.target.value || null
store.setScenarioFilter(value)
store.loadDashboard()
}
function pct(rate) {
return Math.round((rate ?? 0) * 100)
}
</script>
<template>
@@ -48,9 +82,45 @@ function changePeriod(days) {
<Button variant="secondary" size="sm" :disabled="loading" @click="store.loadDashboard()">
Refresh
</Button>
<Button
variant="secondary"
size="sm"
:disabled="loading || feedbackRows.length === 0"
@click="store.exportCsv()"
>
Export CSV
</Button>
</div>
</div>
<div
v-if="showScenarioAttribution"
class="flex flex-wrap items-end gap-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
>
<label class="block min-w-[12rem] flex-1">
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Scenario
</span>
<select
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
:value="selectedScenarioId ?? ''"
@change="changeScenario"
>
<option value="">All scenarios</option>
<option
v-for="id in scenarioOptions"
:key="id"
:value="id"
>
{{ id }}
</option>
</select>
</label>
<p class="text-xs text-gray-500 dark:text-gray-400">
Filter feedback by the simulation scenario that produced the alert.
</p>
</div>
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
<!-- KPI cards -->
@@ -152,5 +222,34 @@ function changePeriod(days) {
</p>
</div>
</Card>
<!-- By scenario (simulation attribution) -->
<Card v-if="showScenarioAttribution">
<h2 class="mb-4 text-lg font-semibold dark:text-white">By Scenario</h2>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<div
v-for="row in byScenario"
:key="row.scenarioId ?? 'non-simulated'"
class="flex flex-col gap-2 py-4 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span class="text-sm font-medium dark:text-white">
{{ row.scenarioId ?? 'Non-simulated' }}
</span>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
({{ row.totalFeedback }} rating{{ row.totalFeedback === 1 ? '' : 's' }})
</span>
</div>
<div class="flex flex-wrap gap-2">
<Badge variant="success" size="xs">{{ pct(row.usefulRate) }}% useful</Badge>
<Badge variant="critical" size="xs">{{ pct(row.falsePositiveRate) }}% FP</Badge>
<Badge variant="info" size="xs">{{ pct(row.wouldActRate) }}% would-act</Badge>
</div>
</div>
<p v-if="!byScenario.length" class="py-4 text-sm text-gray-400">
No feedback in this period. Rate alerts in Alert Center to populate scenario attribution.
</p>
</div>
</Card>
</div>
</template>
</template>