main
vue 181 lines 5.64 KB
Raw
1 <template>
2 <n-spin :show="loading" class="min-h-40">
3 <div class="flex flex-col gap-6">
4 <AlertReportReviewPanelReplay :report :existing-review />
5
6 <AlertReportReviewPanelForm v-model:form="form" :report />
7
8 <AlertReportReviewPanelIocs v-model:state="iocState" v-model:iocs="iocs" :report />
9
10 <!-- Submit review -->
11 <div class="flex flex-col gap-0">
12 <div class="flex items-center justify-between gap-3">
13 <n-button text size="large" @click="showTeachPalace = !showTeachPalace">
14 <template #icon>
15 <Icon
16 name="carbon:chevron-right"
17 class="transition-transform duration-300"
18 :class="{ 'rotate-90': showTeachPalace }"
19 />
20 </template>
21 Teach the palace
22 </n-button>
23 <n-button :disabled="!canSubmit" :loading="submitting" type="primary" @click="handleSubmit">
24 {{ submitting ? "Saving..." : existingReview ? "Update review" : "Submit review" }}
25 </n-button>
26 </div>
27
28 <!-- Inline teach-the-palace -->
29 <n-collapse-transition :show="showTeachPalace">
30 <div class="pt-6">
31 <AlertReportReviewPanelTeachThePalace :report :existing-review />
32 </div>
33 </n-collapse-transition>
34 </div>
35 </div>
36 </n-spin>
37 </template>
38
39 <script setup lang="ts">
40 import type { FormState } from "./AlertReportReviewPanelForm.vue"
41 import type { IocState } from "./AlertReportReviewPanelIocs.vue"
42 import type {
43 AiAnalystIoc,
44 AiAnalystReport,
45 AiAnalystReview,
46 IocVerdictCorrection,
47 SubmitReviewPayload
48 } from "@/types/aiAnalyst.d"
49 import type { ApiError } from "@/types/common"
50 import { NButton, NCollapseTransition, NSpin, useMessage } from "naive-ui"
51 import { computed, onBeforeMount, ref, toRefs } from "vue"
52 import Api from "@/api"
53 import Icon from "@/components/common/Icon.vue"
54 import { getApiErrorMessage } from "@/utils"
55 import AlertReportReviewPanelForm from "./AlertReportReviewPanelForm.vue"
56 import AlertReportReviewPanelIocs from "./AlertReportReviewPanelIocs.vue"
57 import AlertReportReviewPanelReplay from "./AlertReportReviewPanelReplay.vue"
58 import AlertReportReviewPanelTeachThePalace from "./AlertReportReviewPanelTeachThePalace.vue"
59
60 const props = defineProps<{
61 report: AiAnalystReport
62 }>()
63
64 const { report } = toRefs(props)
65
66 const message = useMessage()
67 const loading = ref(false)
68 const submitting = ref(false)
69
70 const iocState = ref<Map<number, IocState>>(new Map())
71 const iocs = ref<AiAnalystIoc[]>([])
72
73 const existingReview = ref<AiAnalystReview | null>(null)
74
75 const form = ref<FormState>({
76 overall_verdict: null,
77 template_choice: null,
78 rating_instructions: 3,
79 rating_artifacts: 3,
80 rating_severity: 3,
81 missing_steps: "",
82 suggested_edits: ""
83 })
84
85 // --- Teach the palace ---
86 const showTeachPalace = ref(false)
87
88 // At least overall_verdict must be set
89 const canSubmit = computed(() => form.value.overall_verdict !== null)
90
91 function hydrateFromReview(r: AiAnalystReview) {
92 form.value.overall_verdict = (r.overall_verdict as "up" | "down" | null) ?? null
93 form.value.template_choice = (r.template_choice as "correct" | "wrong" | "partial" | null) ?? null
94 form.value.rating_instructions = r.rating_instructions ?? 3
95 form.value.rating_artifacts = r.rating_artifacts ?? 3
96 form.value.rating_severity = r.rating_severity ?? 3
97 form.value.missing_steps = r.missing_steps ?? ""
98 form.value.suggested_edits = r.suggested_edits ?? ""
99
100 const next = new Map<number, IocState>()
101
102 for (const ir of r.ioc_reviews || []) {
103 next.set(ir.ioc_id, { verdict_correct: ir.verdict_correct, note: ir.note ?? "" })
104 }
105
106 iocState.value = next
107 }
108
109 async function loadReview() {
110 loading.value = true
111
112 try {
113 const res = await Api.aiAnalyst.getMyReview(report.value.id)
114
115 if (res.data.success) {
116 existingReview.value = res.data.review ?? null
117 if (existingReview.value) hydrateFromReview(existingReview.value)
118 }
119 } catch (err) {
120 message.error(getApiErrorMessage(err as ApiError) || "Failed to load review data")
121 } finally {
122 loading.value = false
123 }
124 }
125
126 function buildReviewPayload(): SubmitReviewPayload {
127 const ioc_reviews: IocVerdictCorrection[] = []
128
129 for (const ioc of iocs.value) {
130 const st = iocState.value.get(ioc.id)
131 if (!st) continue
132 // Only include corrections that represent meaningful input:
133 // verdict marked wrong, OR reviewer left a note.
134 if (st.verdict_correct === false || st.note.trim().length > 0) {
135 ioc_reviews.push({
136 ioc_id: ioc.id,
137 verdict_correct: st.verdict_correct,
138 ...(st.note.trim() ? { note: st.note.trim() } : {})
139 })
140 }
141 }
142
143 return {
144 ...(form.value.overall_verdict ? { overall_verdict: form.value.overall_verdict } : {}),
145 ...(form.value.template_choice ? { template_choice: form.value.template_choice } : {}),
146 ...(report.value.report_markdown && existingReview.value?.template_used
147 ? { template_used: existingReview.value.template_used }
148 : {}),
149 rating_instructions: form.value.rating_instructions,
150 rating_artifacts: form.value.rating_artifacts,
151 rating_severity: form.value.rating_severity,
152 ...(form.value.missing_steps.trim() ? { missing_steps: form.value.missing_steps.trim() } : {}),
153 ...(form.value.suggested_edits.trim() ? { suggested_edits: form.value.suggested_edits.trim() } : {}),
154 ioc_reviews
155 }
156 }
157
158 async function handleSubmit() {
159 if (!canSubmit.value) return
160
161 submitting.value = true
162
163 try {
164 const res = await Api.aiAnalyst.submitReview(report.value.id, buildReviewPayload())
165 if (res.data.success) {
166 existingReview.value = res.data.review
167 message.success(res.data.message || "Review saved")
168 } else {
169 message.warning(res.data.message || "Failed to save review")
170 }
171 } catch (err) {
172 message.error(getApiErrorMessage(err as ApiError) || "Failed to save review")
173 } finally {
174 submitting.value = false
175 }
176 }
177
178 onBeforeMount(() => {
179 loadReview()
180 })
181 </script>