feat: Add support for feature spotlight components (#11012)

This commit is contained in:
Sivin Varghese
2025-03-19 01:47:42 +05:30
committed by GitHub
parent 8291c84cc3
commit 9d49b69f2e
27 changed files with 4296 additions and 8 deletions

View File

@@ -54,6 +54,17 @@
@apply text-n-blue-text;
}
.custom-dashed-border {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23E2E3E7' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.dark .custom-dashed-border {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23343434' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E");
}
// scss-lint:disable PropertySortOrder
@layer base {
/* NEXT COLORS START */

View File

@@ -72,10 +72,18 @@ const handlePageChange = event => {
<div
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
>
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
<slot name="headerTitle" />
</span>
<div class="flex gap-4 items-center">
<slot name="headerTitle">
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
</span>
</slot>
<div v-if="!isEmpty" class="flex items-center gap-2">
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
<slot name="knowMore" />
</div>
</div>
<div
v-if="!showPaywall"
v-on-clickaway="() => emit('close')"

View File

@@ -2,6 +2,7 @@
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import AssistantCard from 'dashboard/components-next/captain/assistant/AssistantCard.vue';
import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/FeatureSpotlight.vue';
import { assistantsList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
const emit = defineEmits(['click']);
@@ -12,6 +13,14 @@ const onClick = () => {
</script>
<template>
<FeatureSpotlight
:title="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
fallback-thumbnail="/assets/images/dashboard/captain/assistant-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-dark.svg"
learn-more-url="https://chwt.app/captain-assistant"
class="mb-8"
/>
<EmptyStateLayout
:title="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.TITLE')"
:subtitle="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.SUBTITLE')"

View File

@@ -2,6 +2,7 @@
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import DocumentCard from 'dashboard/components-next/captain/assistant/DocumentCard.vue';
import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/FeatureSpotlight.vue';
import { documentsList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
const emit = defineEmits(['click']);
@@ -12,6 +13,14 @@ const onClick = () => {
</script>
<template>
<FeatureSpotlight
:title="$t('CAPTAIN.DOCUMENTS.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.DOCUMENTS.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
fallback-thumbnail="/assets/images/dashboard/captain/document-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/document-dark.svg"
learn-more-url="https://chwt.app/captain-document"
class="mb-8"
/>
<EmptyStateLayout
:title="$t('CAPTAIN.DOCUMENTS.EMPTY_STATE.TITLE')"
:subtitle="$t('CAPTAIN.DOCUMENTS.EMPTY_STATE.SUBTITLE')"

View File

@@ -2,6 +2,7 @@
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/FeatureSpotlight.vue';
import { responsesList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
const emit = defineEmits(['click']);
@@ -12,6 +13,14 @@ const onClick = () => {
</script>
<template>
<FeatureSpotlight
:title="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
fallback-thumbnail="/assets/images/dashboard/captain/faqs-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/faqs-dark.svg"
learn-more-url="https://chwt.app/captain-faq"
class="mb-8"
/>
<EmptyStateLayout
:title="$t('CAPTAIN.RESPONSES.EMPTY_STATE.TITLE')"
:subtitle="$t('CAPTAIN.RESPONSES.EMPTY_STATE.SUBTITLE')"

View File

@@ -0,0 +1,38 @@
<script setup>
import FeatureSpotlight from './FeatureSpotlight.vue';
</script>
<template>
<Story
title="Components/FeatureSpotlight/Default"
:layout="{ type: 'grid', width: '1000px' }"
>
<Variant title="Default with learn more URL">
<div class="p-6 bg-n-background">
<FeatureSpotlight
title="Captain Assistant"
note="Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
video-url=""
thumbnail=""
fallback-thumbnail="/assets/images/dashboard/captain/assistant-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-dark.svg"
learn-more-url="https://www.chatwoot.com/hc/user-guide/articles/1738101547-creating-an-assistant-with-captain"
/>
</div>
</Variant>
<Variant title="With Video URL and Thumbnail">
<div class="p-6 bg-n-background">
<FeatureSpotlight
title="Captain Assistant"
note="Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
video-url="https://www.youtube.com/watch?v=E4xUHyAAktY"
thumbnail="https://i.ytimg.com/an_webp/E4xUHyAAktY/mqdefault_6s.webp?du=3000&sqp=CJaKmL4G&rs=AOn4CLCmfy1TMOcW4UsjQTgyKRp4TSGZgg"
fallback-thumbnail="/assets/images/dashboard/captain/assistant-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-dark.svg"
learn-more-url="https://www.chatwoot.com/hc/user-guide/articles/1738101547-creating-an-assistant-with-captain"
/>
</div>
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,95 @@
<script setup>
import { ref } from 'vue';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
title: { type: String, default: '' },
note: { type: String, default: '' },
videoUrl: { type: String, default: '' },
thumbnail: { type: String, default: '' },
fallbackThumbnail: { type: String, default: '' },
fallbackThumbnailDark: { type: String, default: '' },
learnMoreUrl: { type: String, default: '' },
});
const imageError = ref(false);
const handleImageError = () => {
imageError.value = true;
};
const openLink = link => {
if (link) {
window.open(link, '_blank');
}
};
</script>
<template>
<section class="custom-dashed-border rounded-2xl py-5 px-6">
<div class="flex flex-col md:flex-row items-start md:items-center gap-6">
<div
class="flex-shrink-0 bg-gray-800 w-[7.5rem] h-[6.5rem] rounded-lg flex items-center justify-center overflow-hidden"
>
<img
v-if="!imageError && thumbnail"
:src="thumbnail"
:alt="title"
draggable="false"
class="w-full h-full object-cover rounded-lg"
loading="lazy"
@error="handleImageError"
/>
<template v-else>
<img
v-if="fallbackThumbnailDark"
:src="fallbackThumbnailDark"
:alt="title"
draggable="false"
class="w-full h-full object-cover hidden dark:block rounded-lg"
loading="lazy"
/>
<img
v-if="fallbackThumbnail"
:src="fallbackThumbnail"
:alt="title"
draggable="false"
class="w-full h-full object-cover block dark:hidden rounded-lg"
loading="lazy"
/>
</template>
</div>
<div class="flex flex-col flex-1 gap-3 ltr:pr-8 rtl:pl-8">
<p v-if="note" class="text-n-slate-12 text-sm mb-0">{{ note }}</p>
<div class="flex gap-3">
<slot name="actions">
<Button
v-if="videoUrl"
:label="$t('FEATURE_SPOTLIGHT.WATCH_VIDEO')"
sm
faded
slate
icon="i-lucide-circle-play"
@click="openLink(videoUrl)"
/>
<Button
v-if="learnMoreUrl"
:label="$t('FEATURE_SPOTLIGHT.LEARN_MORE')"
sm
faded
slate
trailing-icon
icon="i-lucide-arrow-up-right"
@click="openLink(learnMoreUrl)"
/>
</slot>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,44 @@
<script setup>
import FeatureSpotlightPopover from './FeatureSpotlightPopover.vue';
</script>
<template>
<Story
title="Components/FeatureSpotlight/Popup"
:layout="{ type: 'grid', width: '800px' }"
>
<Variant title="Default with learn more URL">
<div class="p-6 h-[450px] bg-n-background">
<div class="flex gap-8">
<FeatureSpotlightPopover
button-label="Learn about Assistant"
title="Captain Assistant"
note="Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
video-url=""
thumbnail=""
fallback-thumbnail="/assets/images/dashboard/captain/assistant-popover-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-popover-dark.svg"
learn-more-url="https://www.chatwoot.com/hc/user-guide/articles/1738101547-creating-an-assistant-with-captain"
/>
</div>
</div>
</Variant>
<Variant title="With Video Thumbnail and URL">
<div class="p-6 h-[450px] bg-n-background">
<div class="flex gap-8">
<FeatureSpotlightPopover
button-label="Learn about Assistant"
title="Captain Assistant"
note="Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
video-url="https://www.youtube.com/watch?v=E4xUHyAAktY"
thumbnail="https://i.ytimg.com/an_webp/E4xUHyAAktY/mqdefault_6s.webp?du=3000&sqp=CJaKmL4G&rs=AOn4CLCmfy1TMOcW4UsjQTgyKRp4TSGZgg"
fallback-thumbnail="/assets/images/dashboard/captain/assistant-popover-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-popover-dark.svg"
learn-more-url="https://www.chatwoot.com/hc/user-guide/articles/1738101547-creating-an-assistant-with-captain"
/>
</div>
</div>
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,125 @@
<script setup>
import { ref } from 'vue';
import { useToggle } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
buttonLabel: { type: String, default: '' },
title: { type: String, default: '' },
note: { type: String, default: '' },
videoUrl: { type: String, default: '' },
thumbnail: { type: String, default: '' },
fallbackThumbnail: { type: String, default: '' },
fallbackThumbnailDark: { type: String, default: '' },
learnMoreUrl: { type: String, default: '' },
});
const imageError = ref(false);
const [isPopupVisible, togglePopup] = useToggle();
const handleImageError = () => {
imageError.value = true;
};
const openLink = link => {
if (link) {
window.open(link, '_blank');
}
};
</script>
<template>
<div class="relative">
<Button
id="togglePopup"
:label="buttonLabel"
slate
ghost
sm
:class="{ 'bg-n-alpha-2': isPopupVisible }"
@click="togglePopup(!isPopupVisible)"
/>
<div
v-if="isPopupVisible"
v-on-click-outside="[
() => isPopupVisible && (isPopupVisible = false),
{ ignore: ['#togglePopup'] },
]"
>
<section
class="absolute top-full mt-6 ltr:left-0 rtl:right-0 outline outline-1 outline-n-weak bg-n-alpha-3 backdrop-blur-[100px] rounded-xl p-4 w-80"
>
<div
class="absolute -top-[0.77rem] ltr:left-12 rtl:right-12 w-6 h-6 ltr:rotate-45 rtl:-rotate-45 rtl:rounded-tr ltr:rounded-tl rtl:border-r ltr:border-l border-t border-n-weak bg-n-alpha-3 z-10"
/>
<div class="relative flex flex-col items-start gap-4 z-20">
<div class="flex-shrink-0 bg-gray-800 w-full h-[7.5rem] rounded-lg">
<img
v-if="!imageError && thumbnail"
:src="thumbnail"
:alt="title"
draggable="false"
loading="lazy"
class="w-full h-full object-cover rounded-lg"
@error="handleImageError"
/>
<template v-else>
<img
v-if="fallbackThumbnailDark"
:src="fallbackThumbnailDark"
:alt="title"
draggable="false"
loading="lazy"
class="w-full h-full object-cover hidden dark:block"
/>
<img
v-if="fallbackThumbnail"
:src="fallbackThumbnail"
:alt="title"
draggable="false"
loading="lazy"
class="w-full h-full object-cover block dark:hidden"
/>
</template>
</div>
<p v-if="note" class="text-n-slate-12 text-start text-sm mb-0">
{{ note }}
</p>
<div class="flex gap-3 justify-between w-full">
<slot name="actions">
<Button
v-if="videoUrl"
:label="$t('FEATURE_SPOTLIGHT.WATCH_VIDEO')"
sm
faded
slate
icon="i-lucide-circle-play"
class="w-full"
@click="openLink(videoUrl)"
/>
<Button
v-if="learnMoreUrl"
:label="$t('FEATURE_SPOTLIGHT.LEARN_MORE')"
sm
faded
slate
trailing-icon
class="w-full"
icon="i-lucide-arrow-up-right"
@click="openLink(learnMoreUrl)"
/>
</slot>
</div>
</div>
</section>
</div>
</div>
</template>

View File

@@ -39,5 +39,9 @@
},
"LABEL": {
"TAG_BUTTON": "tag"
},
"FEATURE_SPOTLIGHT": {
"LEARN_MORE": "Learn more",
"WATCH_VIDEO": "Watch video"
}
}

View File

@@ -308,6 +308,7 @@
},
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "Send message...",
"LOADER": "Captain is thinking",
@@ -387,7 +388,11 @@
},
"EMPTY_STATE": {
"TITLE": "No assistants available",
"SUBTITLE": "Create an assistant to provide quick and accurate responses to your users. It can learn from your help articles and past conversations."
"SUBTITLE": "Create an assistant to provide quick and accurate responses to your users. It can learn from your help articles and past conversations.",
"FEATURE_SPOTLIGHT": {
"TITLE": "Captain Assistant",
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
}
}
},
"DOCUMENTS": {
@@ -429,7 +434,11 @@
},
"EMPTY_STATE": {
"TITLE": "No documents available",
"SUBTITLE": "Documents are used by your assistant to generate FAQs. You can import documents to provide context for your assistant."
"SUBTITLE": "Documents are used by your assistant to generate FAQs. You can import documents to provide context for your assistant.",
"FEATURE_SPOTLIGHT": {
"TITLE": "Captain Document",
"NOTE": "A document in Captain serves as a knowledge resource for the assistant. By connecting your help center or guides, Captain can analyze the content and provide accurate responses for customer inquiries."
}
}
},
"RESPONSES": {
@@ -507,7 +516,11 @@
},
"EMPTY_STATE": {
"TITLE": "No FAQs Found",
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually."
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually.",
"FEATURE_SPOTLIGHT": {
"TITLE": "Captain FAQ",
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
}
}
},
"INBOXES": {

View File

@@ -9,6 +9,7 @@ import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
import AssistantPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue';
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
import LimitBanner from 'dashboard/components-next/captain/pageComponents/response/LimitBanner.vue';
import { useRouter } from 'vue-router';
@@ -82,6 +83,16 @@ onMounted(() => store.dispatch('captainAssistants/get'));
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@click="handleCreate"
>
<template #knowMore>
<FeatureSpotlightPopover
:button-label="$t('CAPTAIN.HEADER_KNOW_MORE')"
:title="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
fallback-thumbnail="/assets/images/dashboard/captain/assistant-popover-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-popover-dark.svg"
learn-more-url="https://chwt.app/captain-assistant"
/>
</template>
<template #emptyState>
<AssistantPageEmptyState @click="handleCreate" />
</template>

View File

@@ -79,7 +79,9 @@ onMounted(() =>
<template v-if="!isFetchingAssistant" #headerTitle>
<div class="flex flex-row items-center gap-4">
<BackButton compact />
<span class="flex items-center gap-1 text-lg">
<span
class="flex items-center gap-1 text-lg font-medium text-n-slate-12"
>
{{ assistant.name }}
<span class="i-lucide-chevron-right text-xl text-n-slate-10" />
{{ $t('CAPTAIN.INBOXES.HEADER') }}

View File

@@ -11,6 +11,7 @@ import RelatedResponses from 'dashboard/components-next/captain/pageComponents/d
import CreateDocumentDialog from 'dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue';
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
import DocumentPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue';
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
const store = useStore();
@@ -115,6 +116,17 @@ onMounted(() => {
@update:current-page="onPageChange"
@click="handleCreateDocument"
>
<template #knowMore>
<FeatureSpotlightPopover
:button-label="$t('CAPTAIN.HEADER_KNOW_MORE')"
:title="$t('CAPTAIN.DOCUMENTS.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.DOCUMENTS.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
fallback-thumbnail="/assets/images/dashboard/captain/document-popover-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/document-popover-dark.svg"
learn-more-url="https://chwt.app/captain-document"
/>
</template>
<template #emptyState>
<DocumentPageEmptyState @click="handleCreateDocument" />
</template>

View File

@@ -18,6 +18,7 @@ import AssistantSelector from 'dashboard/components-next/captain/pageComponents/
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
import LimitBanner from 'dashboard/components-next/captain/pageComponents/response/LimitBanner.vue';
const router = useRouter();
@@ -247,6 +248,17 @@ onMounted(() => {
@update:current-page="onPageChange"
@click="handleCreate"
>
<template #knowMore>
<FeatureSpotlightPopover
:button-label="$t('CAPTAIN.HEADER_KNOW_MORE')"
:title="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
:note="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
fallback-thumbnail="/assets/images/dashboard/captain/faqs-popover-light.svg"
fallback-thumbnail-dark="/assets/images/dashboard/captain/faqs-popover-dark.svg"
learn-more-url="https://chwt.app/captain-faq"
/>
</template>
<template #emptyState>
<ResponsePageEmptyState @click="handleCreate" />
</template>