| 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> |