|
| 1 | +<template> |
| 2 | + <dialog ref="modal" class="modal" @close="onClose"> |
| 3 | + <div class="modal-box max-w-md p-8"> |
| 4 | + <!-- Step 1: Feedback --> |
| 5 | + <template v-if="step === 'step1'"> |
| 6 | + <div class="flex flex-col items-center gap-2 text-center"> |
| 7 | + <mdi:check-circle class="text-success text-4xl" /> |
| 8 | + <h3 class="text-xl font-bold">{{ $t("cloud.welcome.title") }}</h3> |
| 9 | + <p class="text-base-content/60 text-sm">{{ $t("cloud.welcome.subtitle") }}</p> |
| 10 | + </div> |
| 11 | + |
| 12 | + <div class="divider"></div> |
| 13 | + |
| 14 | + <p class="mb-3 text-sm font-medium">{{ $t("cloud.welcome.question") }}</p> |
| 15 | + |
| 16 | + <textarea |
| 17 | + v-model="intent" |
| 18 | + class="textarea textarea-bordered w-full text-sm" |
| 19 | + rows="3" |
| 20 | + :placeholder="$t('cloud.welcome.placeholder')" |
| 21 | + ></textarea> |
| 22 | + |
| 23 | + <p class="text-base-content/60 mt-3 mb-2 text-xs">{{ $t("cloud.welcome.or-pick") }}</p> |
| 24 | + <div class="flex flex-wrap gap-2"> |
| 25 | + <button |
| 26 | + v-for="option in chipOptions" |
| 27 | + :key="option.value" |
| 28 | + class="btn btn-sm" |
| 29 | + :class="selectedOptions.has(option.value) ? 'btn-primary' : 'btn-outline'" |
| 30 | + @click="toggleOption(option.value)" |
| 31 | + > |
| 32 | + {{ option.label }} |
| 33 | + </button> |
| 34 | + </div> |
| 35 | + |
| 36 | + <button class="btn btn-primary btn-block mt-6" :disabled="submitting" @click="submitFeedback"> |
| 37 | + <span v-if="submitting" class="loading loading-spinner loading-xs"></span> |
| 38 | + {{ $t("cloud.welcome.get-started") }} |
| 39 | + </button> |
| 40 | + <button class="btn btn-ghost btn-block btn-sm mt-1" :disabled="submitting" @click="skipFeedback"> |
| 41 | + {{ $t("cloud.welcome.skip") }} |
| 42 | + </button> |
| 43 | + </template> |
| 44 | + |
| 45 | + <!-- Step 2: Onboarding Checklist --> |
| 46 | + <template v-else-if="step === 'step2'"> |
| 47 | + <h3 class="text-xl font-bold">{{ $t("cloud.welcome.step2-title") }}</h3> |
| 48 | + <p class="text-base-content/60 mt-1 text-sm">{{ $t("cloud.welcome.step2-subtitle") }}</p> |
| 49 | + |
| 50 | + <div class="mt-6 space-y-4"> |
| 51 | + <div v-for="(item, index) in checklistItems" :key="index" class="flex gap-3"> |
| 52 | + <div |
| 53 | + class="flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-bold" |
| 54 | + :class="index === 0 ? 'bg-primary text-primary-content' : 'bg-base-200 text-base-content/60'" |
| 55 | + > |
| 56 | + {{ index + 1 }} |
| 57 | + </div> |
| 58 | + <div> |
| 59 | + <component |
| 60 | + :is="item.href ? 'a' : 'p'" |
| 61 | + class="text-sm font-semibold" |
| 62 | + :class="item.href ? 'link link-hover' : ''" |
| 63 | + :href="item.href" |
| 64 | + :target="item.href ? '_blank' : undefined" |
| 65 | + :rel="item.href ? 'noreferrer noopener' : undefined" |
| 66 | + > |
| 67 | + {{ item.title }} |
| 68 | + </component> |
| 69 | + <p class="text-base-content/60 text-xs">{{ item.description }}</p> |
| 70 | + </div> |
| 71 | + </div> |
| 72 | + </div> |
| 73 | + |
| 74 | + <button class="btn btn-primary btn-block mt-6" @click="createFirstAlert"> |
| 75 | + {{ $t("cloud.welcome.create-alert") }} |
| 76 | + </button> |
| 77 | + <button class="btn btn-ghost btn-block btn-sm mt-1" @click="close"> |
| 78 | + {{ $t("cloud.welcome.later") }} |
| 79 | + </button> |
| 80 | + </template> |
| 81 | + </div> |
| 82 | + <form method="dialog" class="modal-backdrop"> |
| 83 | + <button></button> |
| 84 | + </form> |
| 85 | + </dialog> |
| 86 | +</template> |
| 87 | + |
| 88 | +<script lang="ts" setup> |
| 89 | +const cloudUrl = __CLOUD_URL__; |
| 90 | +const { t } = useI18n(); |
| 91 | +const router = useRouter(); |
| 92 | +const route = useRoute(); |
| 93 | +
|
| 94 | +const modal = ref<HTMLDialogElement>(); |
| 95 | +const step = ref<"step1" | "step2">("step1"); |
| 96 | +const intent = ref(""); |
| 97 | +const selectedOptions = ref(new Set<string>()); |
| 98 | +const submitting = ref(false); |
| 99 | +let feedbackSent = false; |
| 100 | +
|
| 101 | +const chipOptions = [ |
| 102 | + { value: "alerts", label: t("cloud.welcome.chip-alerts") }, |
| 103 | + { value: "daily_summary", label: t("cloud.welcome.chip-summary") }, |
| 104 | + { value: "multiple_hosts", label: t("cloud.welcome.chip-hosts") }, |
| 105 | + { value: "something_else", label: t("cloud.welcome.chip-other") }, |
| 106 | +]; |
| 107 | +
|
| 108 | +const checklistItems = computed(() => [ |
| 109 | + { |
| 110 | + title: t("cloud.welcome.checklist-alert-title"), |
| 111 | + description: t("cloud.welcome.checklist-alert-desc"), |
| 112 | + }, |
| 113 | + { |
| 114 | + title: t("cloud.welcome.checklist-notify-title"), |
| 115 | + description: t("cloud.welcome.checklist-notify-desc"), |
| 116 | + href: `${cloudUrl}/channels`, |
| 117 | + }, |
| 118 | + { |
| 119 | + title: t("cloud.welcome.checklist-agent-title"), |
| 120 | + description: t("cloud.welcome.checklist-agent-desc"), |
| 121 | + href: `${cloudUrl}/assistant`, |
| 122 | + }, |
| 123 | +]); |
| 124 | +
|
| 125 | +function toggleOption(value: string) { |
| 126 | + const next = new Set(selectedOptions.value); |
| 127 | + if (next.has(value)) { |
| 128 | + next.delete(value); |
| 129 | + } else { |
| 130 | + next.add(value); |
| 131 | + } |
| 132 | + selectedOptions.value = next; |
| 133 | +} |
| 134 | +
|
| 135 | +async function postFeedback(skipped: boolean) { |
| 136 | + try { |
| 137 | + await fetch(withBase("/api/cloud/feedback"), { |
| 138 | + method: "POST", |
| 139 | + headers: { "Content-Type": "application/json" }, |
| 140 | + body: JSON.stringify({ |
| 141 | + source: "welcome_modal", |
| 142 | + intent: skipped ? undefined : intent.value || undefined, |
| 143 | + selectedOptions: skipped ? undefined : Array.from(selectedOptions.value), |
| 144 | + skipped, |
| 145 | + }), |
| 146 | + }); |
| 147 | + } catch { |
| 148 | + // Feedback failure should not block the user |
| 149 | + } |
| 150 | +} |
| 151 | +
|
| 152 | +const onNotificationsPage = computed(() => route.path === "/notifications"); |
| 153 | +
|
| 154 | +async function submitFeedback() { |
| 155 | + submitting.value = true; |
| 156 | + feedbackSent = true; |
| 157 | + await postFeedback(false); |
| 158 | + submitting.value = false; |
| 159 | + if (onNotificationsPage.value) { |
| 160 | + createFirstAlert(); |
| 161 | + } else { |
| 162 | + step.value = "step2"; |
| 163 | + } |
| 164 | +} |
| 165 | +
|
| 166 | +async function skipFeedback() { |
| 167 | + submitting.value = true; |
| 168 | + feedbackSent = true; |
| 169 | + await postFeedback(true); |
| 170 | + submitting.value = false; |
| 171 | + if (onNotificationsPage.value) { |
| 172 | + createFirstAlert(); |
| 173 | + } else { |
| 174 | + step.value = "step2"; |
| 175 | + } |
| 176 | +} |
| 177 | +
|
| 178 | +function createFirstAlert() { |
| 179 | + close(); |
| 180 | + router.push({ path: "/notifications", query: { action: "create-alert" } }); |
| 181 | +} |
| 182 | +
|
| 183 | +function open() { |
| 184 | + step.value = "step1"; |
| 185 | + intent.value = ""; |
| 186 | + selectedOptions.value = new Set(); |
| 187 | + feedbackSent = false; |
| 188 | + modal.value?.showModal(); |
| 189 | +} |
| 190 | +
|
| 191 | +function close() { |
| 192 | + modal.value?.close(); |
| 193 | +} |
| 194 | +
|
| 195 | +function onClose() { |
| 196 | + if (step.value === "step1" && !feedbackSent) { |
| 197 | + feedbackSent = true; |
| 198 | + postFeedback(true); |
| 199 | + } |
| 200 | +} |
| 201 | +
|
| 202 | +defineExpose({ open }); |
| 203 | +</script> |
0 commit comments