103 lines
4.3 KiB
Vue
103 lines
4.3 KiB
Vue
<script setup>
|
|
import { storeToRefs } from 'pinia'
|
|
import { useFeedbackStore } from '@/stores/feedback'
|
|
import { alertTypeLabel } from '@/api/normalize'
|
|
import Badge from '@/components/ui/Badge.vue'
|
|
import Button from '@/components/ui/Button.vue'
|
|
import Card from '@/components/ui/Card.vue'
|
|
|
|
const feedbackStore = useFeedbackStore()
|
|
const { entries, stats, byAlertType } = storeToRefs(feedbackStore)
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-full min-w-0 space-y-8">
|
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<h1 class="text-xl font-bold dark:text-white">Feedback Summary</h1>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Button variant="secondary" size="sm" @click="feedbackStore.exportAsJson()">
|
|
Export JSON
|
|
</Button>
|
|
<Button variant="secondary" size="sm" @click="feedbackStore.exportAsCsv()">
|
|
Export CSV
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Aggregate stats -->
|
|
<div class="grid w-full min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold dark:text-white">{{ stats.total }}</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Total Ratings</div>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold text-green-600">{{ stats.usefulPct }}%</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Useful / Would Act</div>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold text-red-600">{{ stats.fpPct }}%</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">False Positive</div>
|
|
</div>
|
|
</Card>
|
|
<Card>
|
|
<div class="text-center">
|
|
<div class="text-3xl font-bold text-amber-600">
|
|
{{ stats.total - stats.useful - stats.falsePositive }}
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Timing Issues</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Per alert type breakdown -->
|
|
<Card>
|
|
<h2 class="mb-4 text-lg font-semibold dark:text-white">By Alert Type</h2>
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
<div v-for="(feedbacks, alertType) in byAlertType" :key="alertType"
|
|
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">{{ alertTypeLabel(alertType) }}</span>
|
|
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
|
({{ feedbacks.length }} ratings)
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<Badge v-if="feedbacks.filter(f => f.rating === 'useful' || f.rating === 'would-act').length"
|
|
variant="success" size="xs">
|
|
{{ feedbacks.filter(f => f.rating === 'useful' || f.rating === 'would-act').length }} useful
|
|
</Badge>
|
|
<Badge v-if="feedbacks.filter(f => f.rating === 'false-positive').length"
|
|
variant="critical" size="xs">
|
|
{{ feedbacks.filter(f => f.rating === 'false-positive').length }} FP
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<!-- Recent feedback entries -->
|
|
<Card>
|
|
<h2 class="mb-4 text-lg font-semibold dark:text-white">Recent Feedback</h2>
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
<div v-for="entry in [...entries].reverse().slice(0, 20)" :key="entry.alertId"
|
|
class="py-4">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<Badge :variant="entry.rating === 'false-positive' ? 'critical' : entry.rating === 'useful' || entry.rating === 'would-act' ? 'success' : 'warning'" size="xs">
|
|
{{ entry.rating }}
|
|
</Badge>
|
|
<span class="text-sm dark:text-white">{{ alertTypeLabel(entry.alertType) }}</span>
|
|
<span class="text-xs text-gray-400">{{ new Date(entry.timestamp).toLocaleString() }}</span>
|
|
</div>
|
|
<p v-if="entry.notes" class="mt-2 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
|
{{ entry.notes }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</template> |