mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
feat: Add CTAs for AI features (#7538)
This commit is contained in:
@@ -1,41 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isAIIntegrationEnabled" class="position-relative">
|
<div v-if="!isFetchingAppIntegrations">
|
||||||
<woot-button
|
<div v-if="isAIIntegrationEnabled" class="relative">
|
||||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
|
<AIAssistanceCTAButton
|
||||||
icon="wand"
|
v-if="shouldShowAIAssistCTAButton"
|
||||||
color-scheme="secondary"
|
@click="openAIAssist"
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
@click="openAIAssist"
|
|
||||||
/>
|
|
||||||
<woot-modal
|
|
||||||
:show.sync="showAIAssistanceModal"
|
|
||||||
:on-close="hideAIAssistanceModal"
|
|
||||||
>
|
|
||||||
<AIAssistanceModal
|
|
||||||
:ai-option="aiOption"
|
|
||||||
@apply-text="insertText"
|
|
||||||
@close="hideAIAssistanceModal"
|
|
||||||
/>
|
/>
|
||||||
</woot-modal>
|
<woot-button
|
||||||
|
v-else
|
||||||
|
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
|
||||||
|
icon="wand"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="smooth"
|
||||||
|
size="small"
|
||||||
|
@click="openAIAssist"
|
||||||
|
/>
|
||||||
|
<woot-modal
|
||||||
|
:show.sync="showAIAssistanceModal"
|
||||||
|
:on-close="hideAIAssistanceModal"
|
||||||
|
>
|
||||||
|
<AIAssistanceModal
|
||||||
|
:ai-option="aiOption"
|
||||||
|
@apply-text="insertText"
|
||||||
|
@close="hideAIAssistanceModal"
|
||||||
|
/>
|
||||||
|
</woot-modal>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
|
||||||
|
<AIAssistanceCTAButton @click="openAICta" />
|
||||||
|
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal">
|
||||||
|
<AICTAModal @close="hideAICtaModal" />
|
||||||
|
</woot-modal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import AICTAModal from './AICTAModal.vue';
|
||||||
import AIAssistanceModal from './AIAssistanceModal.vue';
|
import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
import adminMixin from 'dashboard/mixins/aiMixin';
|
||||||
|
import aiMixin from 'dashboard/mixins/isAdmin';
|
||||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AIAssistanceModal,
|
AIAssistanceModal,
|
||||||
|
AICTAModal,
|
||||||
|
AIAssistanceCTAButton,
|
||||||
},
|
},
|
||||||
mixins: [aiMixin, eventListenerMixins],
|
mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
showAIAssistanceModal: false,
|
showAIAssistanceModal: false,
|
||||||
|
showAICtaModal: false,
|
||||||
aiOption: '',
|
aiOption: '',
|
||||||
initialMessage: '',
|
initialMessage: '',
|
||||||
}),
|
}),
|
||||||
@@ -43,6 +62,21 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
}),
|
}),
|
||||||
|
isAICTAModalDismissed() {
|
||||||
|
return this.uiSettings.is_open_ai_cta_modal_dismissed;
|
||||||
|
},
|
||||||
|
// Display a AI CTA button for admins if the AI integration has not been added yet and the AI assistance modal has not been dismissed.
|
||||||
|
shouldShowAIAssistCTAButtonForAdmin() {
|
||||||
|
return (
|
||||||
|
this.isAdmin &&
|
||||||
|
!this.isAIIntegrationEnabled &&
|
||||||
|
!this.isAICTAModalDismissed
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
|
||||||
|
shouldShowAIAssistCTAButton() {
|
||||||
|
return this.isAIIntegrationEnabled && !this.isAICTAModalDismissed;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -67,10 +101,22 @@ export default {
|
|||||||
this.showAIAssistanceModal = false;
|
this.showAIAssistanceModal = false;
|
||||||
},
|
},
|
||||||
openAIAssist() {
|
openAIAssist() {
|
||||||
|
// Dismiss the CTA modal if it is not dismissed
|
||||||
|
if (!this.isAICTAModalDismissed) {
|
||||||
|
this.updateUISettings({
|
||||||
|
is_open_ai_cta_modal_dismissed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
this.initialMessage = this.draftMessage;
|
this.initialMessage = this.draftMessage;
|
||||||
const ninja = document.querySelector('ninja-keys');
|
const ninja = document.querySelector('ninja-keys');
|
||||||
ninja.open({ parent: 'ai_assist' });
|
ninja.open({ parent: 'ai_assist' });
|
||||||
},
|
},
|
||||||
|
hideAICtaModal() {
|
||||||
|
this.showAICtaModal = false;
|
||||||
|
},
|
||||||
|
openAICta() {
|
||||||
|
this.showAICtaModal = true;
|
||||||
|
},
|
||||||
onAIAssist(option) {
|
onAIAssist(option) {
|
||||||
this.aiOption = option;
|
this.aiOption = option;
|
||||||
this.showAIAssistanceModal = true;
|
this.showAIAssistanceModal = true;
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<woot-button
|
||||||
|
icon="wand"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="smooth"
|
||||||
|
size="small"
|
||||||
|
class-names="cta-btn cta-btn-light dark:cta-btn-dark hover:cta-btn-light-hover dark:hover:cta-btn-dark-hover"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST') }}
|
||||||
|
</woot-button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="radar-ping-animation absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 right-0 -mt-1 -mr-1 rounded-full w-3 h-3 bg-woot-500 dark:bg-woot-500 opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.$emit('click');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
@tailwind components;
|
||||||
|
@layer components {
|
||||||
|
/* Gradient animation */
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cta-btn {
|
||||||
|
animation: gradient 5s ease infinite;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn-light {
|
||||||
|
background: linear-gradient(
|
||||||
|
255.98deg,
|
||||||
|
rgba(161, 87, 246, 0.2) 15.83%,
|
||||||
|
rgba(71, 145, 247, 0.2) 81.39%
|
||||||
|
),
|
||||||
|
linear-gradient(0deg, #f2f5f8, #f2f5f8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn-dark {
|
||||||
|
background: linear-gradient(
|
||||||
|
255.98deg,
|
||||||
|
rgba(161, 87, 246, 0.2) 15.83%,
|
||||||
|
rgba(71, 145, 247, 0.2) 81.39%
|
||||||
|
),
|
||||||
|
linear-gradient(0deg, #313538, #313538);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn-light-hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
255.98deg,
|
||||||
|
rgba(161, 87, 246, 0.2) 15.83%,
|
||||||
|
rgba(71, 145, 247, 0.2) 81.39%
|
||||||
|
),
|
||||||
|
linear-gradient(0deg, #e3e5e7, #e3e5e7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn-dark-hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
255.98deg,
|
||||||
|
rgba(161, 87, 246, 0.2) 15.83%,
|
||||||
|
rgba(71, 145, 247, 0.2) 81.39%
|
||||||
|
),
|
||||||
|
linear-gradient(0deg, #202425, #202425);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radar ping animation */
|
||||||
|
@keyframes ping {
|
||||||
|
75%,
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.radar-ping-animation {
|
||||||
|
animation: ping 1s ease infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
108
app/javascript/dashboard/components/widgets/AICTAModal.vue
Normal file
108
app/javascript/dashboard/components/widgets/AICTAModal.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-0 min-w-0 flex-1">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
|
||||||
|
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
class="flex flex-wrap flex-col modal-content"
|
||||||
|
@submit.prevent="finishOpenAI"
|
||||||
|
>
|
||||||
|
<div class="mt-2 w-full">
|
||||||
|
<woot-input
|
||||||
|
v-model="value"
|
||||||
|
type="text"
|
||||||
|
:class="{ error: $v.value.$error }"
|
||||||
|
:placeholder="
|
||||||
|
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.KEY_PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.value.$touch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
|
||||||
|
<woot-button variant="link" @click.prevent="openOpenAIDoc">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
|
||||||
|
</woot-button>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<woot-button variant="clear" @click.prevent="onDismiss">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button :is-disabled="$v.value.$invalid">
|
||||||
|
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [aiMixin, alertMixin, uiSettingsMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
value: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
appIntegrations: 'integrations/getAppIntegrations',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
onDismiss() {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
|
||||||
|
);
|
||||||
|
this.updateUISettings({
|
||||||
|
is_open_ai_cta_modal_dismissed: true,
|
||||||
|
});
|
||||||
|
this.onClose();
|
||||||
|
},
|
||||||
|
|
||||||
|
async finishOpenAI() {
|
||||||
|
const payload = {
|
||||||
|
app_id: 'openai',
|
||||||
|
settings: {
|
||||||
|
api_key: this.value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('integrations/createHook', payload);
|
||||||
|
this.alertMessage = this.$t(
|
||||||
|
'INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.SUCCESS_MESSAGE'
|
||||||
|
);
|
||||||
|
this.recordAnalytics(
|
||||||
|
OPEN_AI_EVENTS.ADDED_AI_INTEGRATION_VIA_CTA_BUTTON
|
||||||
|
);
|
||||||
|
this.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error?.response?.data?.message;
|
||||||
|
this.alertMessage =
|
||||||
|
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
|
||||||
|
} finally {
|
||||||
|
this.showAlert(this.alertMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openOpenAIDoc() {
|
||||||
|
window.open('https://www.chatwoot.com/blog/v2-17', '_blank');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -89,6 +89,8 @@ export const OPEN_AI_EVENTS = Object.freeze({
|
|||||||
SIMPLIFY: 'OpenAI: Used simplify',
|
SIMPLIFY: 'OpenAI: Used simplify',
|
||||||
APPLY_LABEL_SUGGESTION: 'OpenAI: Apply label from suggestion',
|
APPLY_LABEL_SUGGESTION: 'OpenAI: Apply label from suggestion',
|
||||||
DISMISS_LABEL_SUGGESTION: 'OpenAI: Dismiss label suggestions',
|
DISMISS_LABEL_SUGGESTION: 'OpenAI: Dismiss label suggestions',
|
||||||
|
ADDED_AI_INTEGRATION_VIA_CTA_BUTTON:
|
||||||
|
'OpenAI: Added AI integration via CTA button',
|
||||||
DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions',
|
DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,18 @@
|
|||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Need help?",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
uiFlags: 'integrations/getUIFlags',
|
||||||
appIntegrations: 'integrations/getAppIntegrations',
|
appIntegrations: 'integrations/getAppIntegrations',
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
replyMode: 'draftMessages/getReplyEditorMode',
|
replyMode: 'draftMessages/getReplyEditorMode',
|
||||||
}),
|
}),
|
||||||
isAIIntegrationEnabled() {
|
isAIIntegrationEnabled() {
|
||||||
return this.appIntegrations.find(
|
return !!this.appIntegrations.find(
|
||||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isFetchingAppIntegrations() {
|
||||||
|
return this.uiFlags.isFetching;
|
||||||
|
},
|
||||||
hookId() {
|
hookId() {
|
||||||
return this.appIntegrations.find(
|
return this.appIntegrations.find(
|
||||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export default {
|
|||||||
if (!items.length) return '---';
|
if (!items.length) return '---';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="cell--social-profiles">
|
<div class="cell--social-profiles flex gap-0.5 items-center">
|
||||||
{items.map(
|
{items.map(
|
||||||
profile =>
|
profile =>
|
||||||
profiles[profile] && (
|
profiles[profile] && (
|
||||||
|
|||||||
Reference in New Issue
Block a user