main
vue 96 lines 2.61 KB
Raw
1 <template>
2 <div class="@container w-full">
3 <div class="grid grid-cols-1 gap-3 @lg:grid-cols-2 @3xl:grid-cols-4">
4 <CardLink
5 v-for="tile of metricTiles"
6 :key="tile.title"
7 size="small"
8 :title="tile.title"
9 :value="tile.value"
10 :subtitle="tile.subtitle"
11 :color="tile.color"
12 />
13 </div>
14 </div>
15 </template>
16
17 <script setup lang="ts">
18 import type { CardLinkColor } from "@/components/common/cards/CardLink.vue"
19 import type { AiAnalystReviewStats } from "@/types/aiAnalyst.d"
20 import { computed, toRefs } from "vue"
21 import CardLink from "@/components/common/cards/CardLink.vue"
22
23 const props = defineProps<{
24 stats: AiAnalystReviewStats
25 }>()
26
27 const { stats } = toRefs(props)
28
29 // Composite avg across the three rubric axes — only counts axes with data.
30 const avgOverall = computed(() => {
31 if (!stats.value) return null
32 const parts = [
33 stats.value.avg_rating_instructions,
34 stats.value.avg_rating_artifacts,
35 stats.value.avg_rating_severity
36 ].filter((v): v is number => v !== null)
37 if (!parts.length) return null
38 return parts.reduce((a, b) => a + b, 0) / parts.length
39 })
40
41 const ratingSubtitle = computed(() => {
42 if (!stats.value) return ""
43 const i = stats.value.avg_rating_instructions
44 const a = stats.value.avg_rating_artifacts
45 const s = stats.value.avg_rating_severity
46 return `instr ${i ?? ""} · artif ${a ?? ""} · sev ${s ?? ""}`
47 })
48
49 interface MetricTileConfig {
50 title: string
51 value: string | number
52 subtitle?: string
53 color?: CardLinkColor
54 }
55
56 const metricTiles = computed<MetricTileConfig[]>(() => {
57 const s = stats.value
58 const avg = avgOverall.value
59
60 return [
61 {
62 title: "Total reviews",
63 value: s.total_reviews,
64 subtitle: `correct ${s.template_choice_correct} · partial ${s.template_choice_partial} · wrong ${s.template_choice_wrong}`
65 },
66 {
67 title: "Thumbs up",
68 value: pctLabel(s.thumbs_up_pct),
69 subtitle: `${s.thumbs_up} up / ${s.thumbs_down} down`,
70 color: pctColor(s.thumbs_up_pct)
71 },
72 {
73 title: "IOC verdict accuracy",
74 value: pctLabel(s.ioc_accuracy.accuracy_pct),
75 subtitle: `${s.ioc_accuracy.correct}/${s.ioc_accuracy.total} IOCs correct`,
76 color: pctColor(s.ioc_accuracy.accuracy_pct)
77 },
78 {
79 title: "Avg rating (overall)",
80 value: avg == null ? "" : `${avg.toFixed(2)} / 5`,
81 subtitle: ratingSubtitle.value
82 }
83 ]
84 })
85
86 function pctLabel(pct: number | null): string {
87 return pct === null || pct === undefined ? "" : `${pct.toFixed(1)}%`
88 }
89
90 function pctColor(pct: number | null): "success" | "warning" | "danger" | undefined {
91 if (pct === null) return undefined
92 if (pct >= 75) return "success"
93 if (pct >= 50) return "warning"
94 return "danger"
95 }
96 </script>