| 1 | <template> |
| 2 | <n-spin :show="loading" class="min-h-[200px]"> |
| 3 | <div v-if="reports.length" class="flex flex-col gap-4"> |
| 4 | <div v-if="reports.length > 1" class="flex flex-col gap-2"> |
| 5 | <n-select v-model:value="selectedIndex" :options="reportOptions" size="small" /> |
| 6 | <div class="flex items-center justify-between gap-2"> |
| 7 | <div> |
| 8 | <div v-if="report" class="flex flex-wrap items-center gap-2"> |
| 9 | <n-tag v-if="report.severity_assessment" :type="severityType" size="small" round> |
| 10 | {{ report.severity_assessment }} |
| 11 | </n-tag> |
| 12 | <n-tag size="small" round> |
| 13 | {{ formatDate(report.created_at, dFormats.datetime) }} |
| 14 | </n-tag> |
| 15 | </div> |
| 16 | </div> |
| 17 | |
| 18 | <span class="text-secondary shrink-0 text-xs"> |
| 19 | {{ reports.length }} report{{ reports.length > 1 ? "s" : "" }} |
| 20 | </span> |
| 21 | </div> |
| 22 | </div> |
| 23 | |
| 24 | <div v-if="report" class="flex flex-col gap-4"> |
| 25 | <CardEntity v-if="report.summary" embedded size="small"> |
| 26 | <div class="text-secondary mb-1 text-sm font-semibold uppercase">Summary</div> |
| 27 | <div class="text-sm"> |
| 28 | {{ report.summary }} |
| 29 | </div> |
| 30 | </CardEntity> |
| 31 | |
| 32 | <n-tabs type="line" animated :tabs-padding="0"> |
| 33 | <n-tab-pane name="report" tab="Full Report" display-directive="show:lazy"> |
| 34 | <div class="**:text-default pt-2 **:text-sm [&_*:last-child]:mb-0!"> |
| 35 | <Markdown :source="report.report_markdown || 'No report content'" breaks /> |
| 36 | </div> |
| 37 | </n-tab-pane> |
| 38 | <n-tab-pane name="actions" tab="Recommended Actions" display-directive="show:lazy"> |
| 39 | <div class="**:text-default pt-2 **:text-sm [&_*:last-child]:mb-0!"> |
| 40 | <Markdown :source="report.recommended_actions || 'No recommended actions'" breaks /> |
| 41 | </div> |
| 42 | </n-tab-pane> |
| 43 | </n-tabs> |
| 44 | </div> |
| 45 | </div> |
| 46 | |
| 47 | <div v-else-if="!loading" class="text-secondary flex flex-col items-center justify-center py-12 text-center"> |
| 48 | <Icon name="carbon:bot" :size="40" class="mb-3 opacity-50" /> |
| 49 | <p class="text-sm">No AI Analyst report available for this alert</p> |
| 50 | </div> |
| 51 | </n-spin> |
| 52 | </template> |
| 53 | |
| 54 | <script setup lang="ts"> |
| 55 | import type { TalonJobData } from "@/types/talon.d" |
| 56 | import { NSelect, NSpin, NTabPane, NTabs, NTag } from "naive-ui" |
| 57 | import { computed, onBeforeMount, ref } from "vue" |
| 58 | import Api from "@/api" |
| 59 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 60 | import Icon from "@/components/common/Icon.vue" |
| 61 | import Markdown from "@/components/common/Markdown.vue" |
| 62 | import { useSettingsStore } from "@/stores/settings" |
| 63 | import dayjs from "@/utils/dayjs" |
| 64 | import { formatDate } from "@/utils/format" |
| 65 | |
| 66 | const props = defineProps<{ |
| 67 | alertId: number |
| 68 | }>() |
| 69 | |
| 70 | const dFormats = useSettingsStore().dateFormat |
| 71 | |
| 72 | const loading = ref(false) |
| 73 | const job = ref<TalonJobData | null>(null) |
| 74 | const selectedIndex = ref(0) |
| 75 | |
| 76 | const reports = computed(() => { |
| 77 | const r = job.value?.reports || [] |
| 78 | // Most recent first |
| 79 | return [...r].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) |
| 80 | }) |
| 81 | |
| 82 | const reportOptions = computed(() => |
| 83 | reports.value.map((r, i) => { |
| 84 | const chunks = [] |
| 85 | |
| 86 | chunks.push(dayjs(r.created_at).fromNow()) |
| 87 | |
| 88 | if (r.severity_assessment) { |
| 89 | chunks.push(r.severity_assessment) |
| 90 | } |
| 91 | |
| 92 | if (r.job_id) { |
| 93 | chunks.push(`#${r.job_id}`) |
| 94 | } |
| 95 | |
| 96 | return { |
| 97 | label: chunks.join(" — "), |
| 98 | value: i |
| 99 | } |
| 100 | }) |
| 101 | ) |
| 102 | |
| 103 | const report = computed(() => reports.value[selectedIndex.value] || null) |
| 104 | |
| 105 | const severityType = computed(() => { |
| 106 | const s = report.value?.severity_assessment?.toLowerCase() |
| 107 | if (s === "critical" || s === "high") return "error" |
| 108 | if (s === "medium") return "warning" |
| 109 | return "info" |
| 110 | }) |
| 111 | |
| 112 | function fetchJob() { |
| 113 | loading.value = true |
| 114 | |
| 115 | Api.talon |
| 116 | .getJob(props.alertId) |
| 117 | .then(res => { |
| 118 | if (res.data.success && res.data.data) { |
| 119 | job.value = res.data.data |
| 120 | } |
| 121 | }) |
| 122 | .finally(() => { |
| 123 | loading.value = false |
| 124 | }) |
| 125 | } |
| 126 | |
| 127 | onBeforeMount(() => { |
| 128 | fetchJob() |
| 129 | }) |
| 130 | </script> |