Compare commits

...
Author SHA1 Message Date
Muhsin Keloth d61eed7169 Add more changes 2023-07-05 15:19:20 +05:30
Muhsin Keloth 464e0a5d35 Add more changes 2023-07-05 13:49:36 +05:30
Muhsin Keloth 146bdd2f83 Update command bar to accomdate more AI options 2023-07-04 15:28:42 +05:30
9 changed files with 593 additions and 45 deletions
@@ -1,7 +1,25 @@
<template>
<div v-if="isAIIntegrationEnabled" class="position-relative">
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="openAIAssist"
/>
<!-- <woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
icon="text-grammar-wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleDropdown"
/> -->
<div v-if="!message">
<woot-button
<!-- <woot-button
v-if="isPrivateNote"
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.SUMMARY_TITLE')"
icon="book-pulse"
@@ -20,53 +38,62 @@
size="small"
:is-loading="uiFlags.reply_suggestion"
@click="processEvent('reply_suggestion')"
/>
/> -->
</div>
<div v-else>
<woot-button
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
icon="text-grammar-wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleDropdown"
/>
<div
v-if="showDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open ai-modal"
>
<h4 class="sub-block-title margin-top-1">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
</h4>
<p>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
</p>
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
</label>
<div class="tone__item">
<select v-model="activeTone" class="status--filter small">
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
{{ tone.value }}
</option>
</select>
</div>
<div class="modal-footer flex-container align-right">
<woot-button variant="clear" size="small" @click="closeDropdown">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
</woot-button>
<woot-button
:is-loading="uiFlags.rephrase"
size="small"
@click="processEvent('rephrase')"
>
{{ buttonText }}
</woot-button>
</div>
<div
v-if="showDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open ai-modal"
>
<h4 class="sub-block-title margin-top-1">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
</h4>
<p>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
</p>
<label>
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
</label>
<div class="tone__item">
<select v-model="activeTone" class="status--filter small">
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
{{ tone.value }}
</option>
</select>
</div>
<div class="modal-footer flex-container align-right">
<woot-button variant="clear" size="small" @click="closeDropdown">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
</woot-button>
<woot-button
:is-loading="uiFlags.rephrase"
size="small"
@click="processEvent('rephrase')"
>
{{ buttonText }}
</woot-button>
</div>
</div>
<woot-modal
:show.sync="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
@apply-text="insertText"
@close="hideAIAssistanceModal"
/>
</woot-modal>
<woot-modal
:show.sync="showAIAssistanceReply"
:on-close="hideAIAssistanceReplyModal"
>
<AIAssistanceReply
@apply-text="insertText"
@close="hideAIAssistanceReplyModal"
/>
</woot-modal>
</div>
</template>
<script>
@@ -75,8 +102,15 @@ import { mixin as clickaway } from 'vue-clickaway';
import OpenAPI from 'dashboard/api/integrations/openapi';
import alertMixin from 'shared/mixins/alertMixin';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { CMD_AI_ASSIST } from '../../routes/dashboard/commands/commandBarBusEvents';
import AIAssistanceModal from './AIAssistanceModal.vue';
import AIAssistanceReply from './AIAssistanceReply.vue';
export default {
components: {
AIAssistanceModal,
AIAssistanceReply,
},
mixins: [alertMixin, clickaway],
props: {
conversationId: {
@@ -94,6 +128,8 @@ export default {
},
data() {
return {
showAIAssistanceModal: false,
showAIAssistanceReply: false,
uiFlags: {
rephrase: false,
reply_suggestion: false,
@@ -134,11 +170,29 @@ export default {
},
},
mounted() {
bus.$on(CMD_AI_ASSIST, this.onAIAssist);
if (!this.appIntegrations.length) {
this.$store.dispatch('integrations/get');
}
},
destroyed() {
bus.$off(CMD_AI_ASSIST, this.onAIAssist);
},
methods: {
hideAIAssistanceReplyModal() {
this.showAIAssistanceReply = false;
},
hideAIAssistanceModal() {
this.showAIAssistanceModal = false;
},
openAIAssist() {
const ninja = document.querySelector('ninja-keys');
ninja.open({ parent: 'ai_assist' });
},
onAIAssist() {
// this.showAIAssistanceModal = true;
this.showAIAssistanceReply = true;
},
toggleDropdown() {
this.showDropdown = !this.showDropdown;
},
@@ -154,6 +208,11 @@ export default {
});
}
},
insertText(message) {
console.log('message', message);
this.$emit('replace-text', message);
},
async processEvent(type = 'rephrase') {
this.uiFlags[type] = true;
try {
@@ -0,0 +1,152 @@
<template>
<div class="column">
<woot-modal-header header-title="Rephrase the content" />
<form class="row modal-content" @submit.prevent="chooseTime">
<div>
<h4 class="sub-block-title margin-top-1">
Draft content
</h4>
</div>
<p>
{{ draftContent }}
</p>
<div class="container">
<h4 class="sub-block-title margin-top-1">
AI generated content
</h4>
</div>
<div class="animation-container margin-top-1">
<div class="ai-typing--wrap ">
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
<label>AI is writing</label>
</div>
<span class="loader" />
<span class="loader" />
<span class="loader" />
</div>
<div class="modal-footer justify-content-end w-full">
<!-- <woot-button
icon="delete-outline"
variant="clear"
@click.prevent="onClose"
>
Discard
</woot-button> -->
<woot-button variant="clear" @click.prevent="onClose">
Discard
</woot-button>
<!-- <woot-button
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
color-scheme="alert"
variant="clear"
icon="arrow-clockwise"
:disabled="!enableButtons"
>Retry
</woot-button> -->
<woot-button :disabled="!enableButtons">
Apply
</woot-button>
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
draftContent:
'I am closing this thread as there is no response. Please feel to create a new one if the issue/query is not resolved.',
generatedContent: '',
};
},
computed: {
aiContent() {
return this.generatedContent.length > 0
? this.generatedContent
: 'Generating content...';
},
enableButtons() {
return this.generatedContent.length > 0;
},
},
mounted() {
setTimeout(() => {
this.generatedContent =
'I am closing this thread as there has been no response. Please feel free to create a new thread if the issue or query remains unresolved.';
}, 6000);
},
methods: {
onClose() {
this.$emit('close');
},
chooseTime() {},
},
};
</script>
<style lang="scss" scoped>
.modal-content {
padding-top: var(--space-small);
}
.container {
width: 100%;
}
.ai-typing--wrap {
display: flex;
align-items: center;
gap: 4px;
.ai-typing--icon {
color: var(--v-500);
}
}
.animation-container {
position: relative;
display: flex;
}
.animation-container label {
display: inline-block;
margin-right: 8px;
color: var(--v-400);
}
.loader {
display: inline-block;
width: 6px;
height: 6px;
margin-right: 4px;
margin-top: 12px;
background-color: var(--v-300);
border-radius: 50%;
animation: bubble-scale 1.2s infinite;
}
.loader:nth-child(2) {
animation-delay: 0.4s;
}
.loader:nth-child(3) {
animation-delay: 0.8s;
}
@keyframes bubble-scale {
0%,
100% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(1);
}
}
</style>
@@ -0,0 +1,175 @@
<template>
<div class="column">
<woot-modal-header header-title="Reply suggestion with AI" />
<form class="row modal-content" @submit.prevent="chooseTime">
<div v-if="!generatedContent" class="animation-container margin-top-1">
<div class="ai-typing--wrap ">
<fluent-icon icon="wand" size="14" class="ai-typing--icon" />
<label>AI is writing</label>
</div>
<span class="loader" />
<span class="loader" />
<span class="loader" />
</div>
<div v-else>
<p>
{{ aiContent }}
</p>
</div>
<div class="modal-footer justify-content-end w-full">
<!-- <woot-button
icon="delete-outline"
variant="clear"
@click.prevent="onClose"
>
Discard
</woot-button> -->
<woot-button variant="clear" @click.prevent="onClose">
Discard
</woot-button>
<!-- <woot-button
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
color-scheme="alert"
variant="clear"
icon="arrow-clockwise"
:disabled="!enableButtons"
>Retry
</woot-button> -->
<woot-button :disabled="!enableButtons" @click="applyContent">
Apply
</woot-button>
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
draftContent:
'I am closing this thread as there is no response. Please feel to create a new one if the issue/query is not resolved.',
generatedContent: '',
};
},
computed: {
aiContent() {
return this.generatedContent.length > 0
? this.generatedContent
: 'Generating content...';
},
enableButtons() {
return this.generatedContent.length > 0;
},
},
mounted() {
setTimeout(() => {
this.generatedContent =
'Hi there! Its great to see you here. How can I be of assistance to you today? Whether you need help with a specific task or have a general question, I am here to lend a hand. Let me know what you need, and I will do my best to provide you with the information or guidance you are looking for.';
}, 5000);
},
methods: {
onClose() {
this.$emit('close');
},
chooseTime() {},
applyContent() {
this.$emit('apply-text', this.generatedContent);
this.$emit('close');
},
},
};
</script>
<style lang="scss" scoped>
.modal-content {
padding-top: var(--space-small);
}
.container {
width: 100%;
}
.ai-typing--wrap {
display: flex;
align-items: center;
gap: 4px;
.ai-typing--icon {
color: var(--v-500);
}
}
.animation-container {
position: relative;
display: flex;
}
.animation-container label {
display: inline-block;
margin-right: 8px;
color: var(--v-400);
}
.loader {
display: inline-block;
width: 6px;
height: 6px;
margin-right: 4px;
margin-top: 12px;
background-color: var(--v-300);
border-radius: 50%;
animation: bubble-scale 1.2s infinite;
}
.loader:nth-child(2) {
animation-delay: 0.4s;
}
.loader:nth-child(3) {
animation-delay: 0.8s;
}
@keyframes bubble-scale {
0%,
100% {
transform: scale(1);
}
25% {
transform: scale(1.3);
}
50% {
transform: scale(1);
}
}
// .typewriter p {
// overflow: hidden; /* Ensures the content is not revealed until the animation */
// border-right: 0.15em solid orange; /* The typwriter cursor */
// margin: 0 auto; /* Gives that scrolling effect as the typing happens */
// animation: typing 3.5s steps(30, end), blink-caret 0.5s step-end infinite;
// }
// /* The typing effect */
// @keyframes typing {
// from {
// width: 0;
// }
// to {
// width: 100%;
// }
// }
// @keyframes blink-caret {
// from,
// to {
// border-color: transparent;
// }
// 50% {
// border-color: orange;
// }
// }
</style>
@@ -0,0 +1,77 @@
<template>
<div class="typing-indicator">
<label>AI is typing</label>
<div class="typing">
<span class="circle scaling" />
<span class="circle scaling" />
<span class="circle scaling" />
</div>
</div>
</template>
<style lang="scss" scoped>
.typing-indicator {
color: #282d3e;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
width: fit-content;
.typing {
display: block;
width: 36px;
height: 16px;
border-radius: 20px;
margin: 0 4px 0 8px;
display: flex;
justify-content: center;
align-items: center;
}
.circle {
display: block;
height: 6px;
width: 6px;
border-radius: 50%;
background-color: #000;
margin: 3px;
&.scaling {
animation: typing 1000ms ease-in-out infinite;
animation-delay: 3600ms;
}
&.bouncing {
animation: bounce 1000ms ease-in-out infinite;
animation-delay: 3600ms;
}
.circle:nth-child(1) {
animation-delay: 0ms;
}
.circle:nth-child(2) {
animation-delay: 333ms;
}
.circle:nth-child(3) {
animation-delay: 666ms;
}
}
}
@keyframes typing {
0% {
transform: scale(1);
}
33% {
transform: scale(1);
}
50% {
transform: scale(1.4);
}
100% {
transform: scale(1);
}
}
</style>
@@ -110,7 +110,8 @@
"SNOOZE_CONVERSATION": "Snooze Conversation",
"ADD_LABEL": "Add label to the conversation",
"REMOVE_LABEL": "Remove label from the conversation",
"SETTINGS": "Settings"
"SETTINGS": "Settings",
"AI_ASSIST": "AI Assist"
},
"COMMANDS": {
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
@@ -132,6 +133,7 @@
"GO_TO_NOTIFICATIONS": "Go to Notifications",
"ADD_LABELS_TO_CONVERSATION": "Add label to the conversation",
"ASSIGN_AN_AGENT": "Assign an agent",
"AI_ASSIST": "AI Assist",
"ASSIGN_PRIORITY": "Assign priority",
"ASSIGN_A_TEAM": "Assign a team",
"MUTE_CONVERSATION": "Mute conversation",
@@ -83,6 +83,7 @@
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"OPEN_AI": {
"AI_ASSIST": "AI Assist",
"TITLE": "Improve With AI",
"SUMMARY_TITLE": "Summary with AI",
"REPLY_TITLE": "Reply suggestion with AI",
@@ -58,3 +58,6 @@ export const ICON_PRIORITY_NONE = `<svg role="img" class="ninja-icon ninja-icon-
<rect width="24" height="24" fill="#F1F5F8"/>
<path d="M13.5686 8L11.1579 16.9562H10L12.4107 8H13.5686Z" fill="#446888"/>
</svg>`;
export const ICON_AI_ASSIST = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m13.314 7.565l-.136.126l-10.48 10.488a2.27 2.27 0 0 0 3.211 3.208L16.388 10.9a2.251 2.251 0 0 0-.001-3.182l-.157-.146a2.25 2.25 0 0 0-2.916-.007Zm-.848 2.961l1.088 1.088l-8.706 8.713a.77.77 0 1 1-1.089-1.088l8.707-8.713Zm4.386 4.48L16.75 15a.75.75 0 0 0-.743.648L16 15.75v.75h-.75a.75.75 0 0 0-.743.648l-.007.102c0 .38.282.694.648.743l.102.007H16v.75c0 .38.282.694.648.743l.102.007a.75.75 0 0 0 .743-.648l.007-.102V18h.75a.75.75 0 0 0 .743-.648L19 17.25a.75.75 0 0 0-.648-.743l-.102-.007h-.75v-.75a.75.75 0 0 0-.648-.743L16.75 15l.102.007Zm-1.553-6.254l.027.027a.751.751 0 0 1 0 1.061l-.711.713l-1.089-1.089l.73-.73a.75.75 0 0 1 1.043.018ZM6.852 5.007L6.75 5a.75.75 0 0 0-.743.648L6 5.75v.75h-.75a.75.75 0 0 0-.743.648L4.5 7.25c0 .38.282.693.648.743L5.25 8H6v.75c0 .38.282.693.648.743l.102.007a.75.75 0 0 0 .743-.648L7.5 8.75V8h.75a.75.75 0 0 0 .743-.648L9 7.25a.75.75 0 0 0-.648-.743L8.25 6.5H7.5v-.75a.75.75 0 0 0-.648-.743L6.75 5l.102.007Zm12-2L18.75 3a.75.75 0 0 0-.743.648L18 3.75v.75h-.75a.75.75 0 0 0-.743.648l-.007.102c0 .38.282.693.648.743L17.25 6H18v.75c0 .38.282.693.648.743l.102.007a.75.75 0 0 0 .743-.648l.007-.102V6h.75a.75.75 0 0 0 .743-.648L21 5.25a.75.75 0 0 0-.648-.743L20.25 4.5h-.75v-.75a.75.75 0 0 0-.648-.743L18.75 3l.102.007Z" fill="currentColor"/></svg>`;
export const ICON_AI_SUMMARY = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15ZM5.5 18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1V18Z" fill="currentColor"/></svg>`;
@@ -14,3 +14,4 @@ export const CMD_TOGGLE_CONTACT_SIDEBAR = 'CMD_TOGGLE_CONTACT_SIDEBAR';
export const CMD_REOPEN_CONVERSATION = 'CMD_REOPEN_CONVERSATION';
export const CMD_RESOLVE_CONVERSATION = 'CMD_RESOLVE_CONVERSATION';
export const CMD_SNOOZE_CONVERSATION = 'CMD_SNOOZE_CONVERSATION';
export const CMD_AI_ASSIST = 'CMD_AI_ASSIST';
@@ -1,6 +1,10 @@
import { mapGetters } from 'vuex';
import wootConstants from 'dashboard/constants/globals';
import { LocalStorage } from 'shared/helpers/localStorage';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import {
CMD_AI_ASSIST,
CMD_MUTE_CONVERSATION,
CMD_REOPEN_CONVERSATION,
CMD_RESOLVE_CONVERSATION,
@@ -26,6 +30,8 @@ import {
ICON_PRIORITY_LOW,
ICON_PRIORITY_MEDIUM,
ICON_PRIORITY_NONE,
ICON_AI_ASSIST,
ICON_AI_SUMMARY,
} from './CommandBarIcons';
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
@@ -337,6 +343,77 @@ export default {
]);
},
AIAssistActions() {
const savedDraftMessages =
LocalStorage.get(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES) || {};
const replyType = 'REPLY';
const key = `draft-${this.currentChat.id}-${replyType}`;
const message = `${savedDraftMessages[key] || ''}`;
let aiOptions = [];
if (!message) {
aiOptions = [
{
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_TITLE'),
key: 'reply_suggestion',
icon: ICON_AI_ASSIST,
},
{
label: this.$t('INTEGRATION_SETTINGS.OPEN_AI.SUMMARY_TITLE'),
key: 'summarize',
icon: ICON_AI_SUMMARY,
},
];
} else {
aiOptions = [
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
key: null,
icon: ICON_PRIORITY_NONE,
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
key: 'urgent',
icon: ICON_PRIORITY_URGENT,
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
key: 'high',
icon: ICON_PRIORITY_HIGH,
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
key: 'medium',
icon: ICON_PRIORITY_MEDIUM,
},
{
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
key: 'low',
icon: ICON_PRIORITY_LOW,
},
];
}
const options = aiOptions.map(item => ({
id: `ai-assist-${item.key}`,
title: item.label,
parent: 'ai_assist',
section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
priority: item,
icon: item.icon,
handler: () => bus.$emit(CMD_AI_ASSIST, item.key),
}));
return [
{
id: 'ai_assist',
title: this.$t('COMMAND_BAR.COMMANDS.AI_ASSIST'),
section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
icon: ICON_AI_ASSIST,
children: options.map(option => option.id),
},
...options,
];
},
conversationHotKeys() {
if (isAConversationRoute(this.$route.name)) {
return [
@@ -346,6 +423,7 @@ export default {
...this.assignTeamActions,
...this.labelActions,
...this.assignPriorityActions,
...this.AIAssistActions,
];
}