main
vue 150 lines 4.65 KB
Raw
1 <template>
2 <n-modal
3 v-model:show="showLocal"
4 :style="{ maxWidth: 'min(720px, 90vw)' }"
5 preset="card"
6 title="Replay with different template"
7 :bordered="false"
8 segmented
9 >
10 <n-spin :show="loading">
11 <div class="flex flex-col gap-4">
12 <div class="text-secondary text-sm">
13 Re-runs the investigation for alert
14 <code class="text-primary">#{{ report.alert_id }}</code>
15 with a forced template. The replay creates its own new job and report via Talon — nothing on this
16 report is modified.
17 </div>
18
19 <div class="flex flex-col gap-2">
20 <div class="flex items-center justify-between gap-2">
21 <div class="text-sm">Choose a template</div>
22 <Badge type="splitted" bright>
23 <template #label>Customer</template>
24 <template #value>{{ report.customer_code }}</template>
25 </Badge>
26 </div>
27 <div v-if="!loading && !templates.length">
28 <n-empty description="No templates available" class="min-h-20 justify-center" />
29 </div>
30 <n-radio-group v-else v-model:value="selectedFilename" class="flex! w-full flex-col gap-2">
31 <n-radio
32 v-for="tpl of templates"
33 :key="tpl.filename"
34 :value="tpl.filename"
35 class="border-default hover:border-primary w-full rounded-lg border p-3 transition-colors [&_.n-radio\_\_label]:grow"
36 :class="selectedFilename === tpl.filename ? 'border-primary bg-primary/5' : ''"
37 >
38 <div class="flex w-full flex-col gap-1 pl-0.5">
39 <div class="flex w-full items-center justify-between gap-3">
40 <span class="font-medium">{{ tpl.filename }}</span>
41 <span class="text-secondary text-xs">{{ formatBytes(tpl.size_bytes) }}</span>
42 </div>
43 <span class="text-secondary text-xs">
44 updated {{ formatDate(tpl.modified_at, "MMM D, YYYY HH:mm") }}
45 </span>
46 </div>
47 </n-radio>
48 </n-radio-group>
49 </div>
50 </div>
51 </n-spin>
52
53 <template #action>
54 <div class="flex w-full items-center justify-end gap-3">
55 <n-button :disabled="submitting" @click="showLocal = false">Cancel</n-button>
56 <n-button type="primary" :disabled="!selectedFilename" :loading="submitting" @click="handleReplay">
57 Replay
58 </n-button>
59 </div>
60 </template>
61 </n-modal>
62 </template>
63
64 <script setup lang="ts">
65 import type { AiAnalystReport } from "@/types/aiAnalyst.d"
66 import type { TalonTemplate } from "@/types/talon.d"
67 import { NButton, NEmpty, NModal, NRadio, NRadioGroup, NSpin, useMessage } from "naive-ui"
68 import { computed, ref, watch } from "vue"
69 import Api from "@/api"
70 import Badge from "@/components/common/Badge.vue"
71 import { formatBytes, formatDate } from "@/utils/format"
72
73 const props = defineProps<{
74 show: boolean
75 report: AiAnalystReport
76 }>()
77
78 const emit = defineEmits<{
79 (e: "update:show", v: boolean): void
80 (e: "replayed", data: Record<string, unknown> | undefined): void
81 }>()
82
83 const message = useMessage()
84 const loading = ref(false)
85 const submitting = ref(false)
86 const templates = ref<TalonTemplate[]>([])
87 const selectedFilename = ref<string | null>(null)
88
89 const showLocal = computed({
90 get: () => props.show,
91 set: (v: boolean) => emit("update:show", v)
92 })
93
94 async function loadTemplates() {
95 loading.value = true
96 selectedFilename.value = null
97
98 try {
99 const res = await Api.talon.getTemplates()
100 if (res.data.success) {
101 templates.value = res.data.templates || []
102 } else {
103 message.warning(res.data.message || "Failed to load templates")
104 templates.value = []
105 }
106 } catch (err: unknown) {
107 const e = err as { response?: { data?: { message?: string } }; message?: string }
108 message.error(e.response?.data?.message || e.message || "Failed to load templates")
109 templates.value = []
110 } finally {
111 loading.value = false
112 }
113 }
114
115 async function handleReplay() {
116 if (!selectedFilename.value) return
117
118 submitting.value = true
119
120 try {
121 const res = await Api.aiAnalyst.replayReport(props.report.id, {
122 template_override: selectedFilename.value,
123 customer_code: props.report.customer_code,
124 sender: "copilot-replay"
125 })
126 if (res.data.success) {
127 message.success(res.data.message || "Replay triggered")
128 emit("replayed", res.data.data)
129 showLocal.value = false
130 } else {
131 message.warning(res.data.message || "Failed to trigger replay")
132 }
133 } catch (err: unknown) {
134 const e = err as { response?: { data?: { message?: string } }; message?: string }
135 message.error(e.response?.data?.message || e.message || "Failed to trigger replay")
136 } finally {
137 submitting.value = false
138 }
139 }
140
141 // Fetch templates each time the modal is opened — fresh mtime + preview,
142 // and covers the case where the user adds a template via the palace flow.
143 watch(
144 () => props.show,
145 v => {
146 if (v) loadTemplates()
147 },
148 { immediate: true }
149 )
150 </script>