Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
- Loading branch information
1 parent
ec65b43
commit 91c1061
Showing
15 changed files
with
565 additions
and
314 deletions.
There are no files selected for viewing
224 changes: 50 additions & 174 deletions
224
app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,204 +1,80 @@ | ||
<template> | ||
<div v-if="isAIIntegrationEnabled" class="position-relative"> | ||
<div v-if="!message"> | ||
<woot-button | ||
v-if="isPrivateNote" | ||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.SUMMARY_TITLE')" | ||
icon="book-pulse" | ||
color-scheme="secondary" | ||
variant="smooth" | ||
size="small" | ||
:is-loading="uiFlags.summarize" | ||
@click="processEvent('summarize')" | ||
<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-modal | ||
:show.sync="showAIAssistanceModal" | ||
:on-close="hideAIAssistanceModal" | ||
> | ||
<AIAssistanceModal | ||
:ai-option="aiOption" | ||
@apply-text="insertText" | ||
@close="hideAIAssistanceModal" | ||
/> | ||
<woot-button | ||
v-else | ||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_TITLE')" | ||
icon="wand" | ||
color-scheme="secondary" | ||
variant="smooth" | ||
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> | ||
</div> | ||
</woot-modal> | ||
</div> | ||
</template> | ||
<script> | ||
import { mixin as clickaway } from 'vue-clickaway'; | ||
import OpenAPI from 'dashboard/api/integrations/openapi'; | ||
import alertMixin from 'shared/mixins/alertMixin'; | ||
import { mapGetters } from 'vuex'; | ||
import AIAssistanceModal from './AIAssistanceModal.vue'; | ||
import aiMixin from 'dashboard/mixins/aiMixin'; | ||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; | ||
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; | ||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers'; | ||
export default { | ||
mixins: [aiMixin, alertMixin, clickaway, eventListenerMixins], | ||
props: { | ||
conversationId: { | ||
type: Number, | ||
default: 0, | ||
}, | ||
message: { | ||
type: String, | ||
default: '', | ||
}, | ||
isPrivateNote: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
}, | ||
data() { | ||
return { | ||
uiFlags: { | ||
rephrase: false, | ||
reply_suggestion: false, | ||
summarize: false, | ||
}, | ||
showDropdown: false, | ||
activeTone: 'professional', | ||
initialMessage: '', | ||
tones: [ | ||
{ | ||
key: 'professional', | ||
value: this.$t( | ||
'INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.PROFESSIONAL' | ||
), | ||
}, | ||
{ | ||
key: 'friendly', | ||
value: this.$t('INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.FRIENDLY'), | ||
}, | ||
], | ||
}; | ||
components: { | ||
AIAssistanceModal, | ||
}, | ||
mixins: [aiMixin, eventListenerMixins], | ||
data: () => ({ | ||
showAIAssistanceModal: false, | ||
aiOption: '', | ||
initialMessage: '', | ||
}), | ||
computed: { | ||
buttonText() { | ||
return this.uiFlags.isRephrasing | ||
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING') | ||
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE'); | ||
}, | ||
...mapGetters({ | ||
currentChat: 'getSelectedChat', | ||
}), | ||
}, | ||
mounted() { | ||
bus.$on(CMD_AI_ASSIST, this.onAIAssist); | ||
this.initialMessage = this.draftMessage; | ||
}, | ||
methods: { | ||
onKeyDownHandler(event) { | ||
const keyPattern = buildHotKeys(event); | ||
const shouldRevertTheContent = | ||
['meta+z', 'ctrl+z'].includes(keyPattern) && !!this.initialMessage; | ||
if (shouldRevertTheContent) { | ||
this.$emit('replace-text', this.initialMessage); | ||
this.initialMessage = ''; | ||
} | ||
}, | ||
toggleDropdown() { | ||
this.showDropdown = !this.showDropdown; | ||
hideAIAssistanceModal() { | ||
this.showAIAssistanceModal = false; | ||
}, | ||
closeDropdown() { | ||
this.showDropdown = false; | ||
openAIAssist() { | ||
this.initialMessage = this.draftMessage; | ||
const ninja = document.querySelector('ninja-keys'); | ||
ninja.open({ parent: 'ai_assist' }); | ||
}, | ||
async processEvent(type = 'rephrase') { | ||
this.uiFlags[type] = true; | ||
try { | ||
const result = await OpenAPI.processEvent({ | ||
hookId: this.hookId, | ||
type, | ||
content: this.message, | ||
tone: this.activeTone, | ||
conversationId: this.conversationId, | ||
}); | ||
const { | ||
data: { message: generatedMessage }, | ||
} = result; | ||
this.initialMessage = this.message; | ||
this.$emit('replace-text', generatedMessage || this.message); | ||
this.closeDropdown(); | ||
this.recordAnalytics(type, { tone: this.activeTone }); | ||
} catch (error) { | ||
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR')); | ||
} finally { | ||
this.uiFlags[type] = false; | ||
} | ||
onAIAssist(option) { | ||
this.aiOption = option; | ||
this.showAIAssistanceModal = true; | ||
}, | ||
insertText(message) { | ||
this.$emit('replace-text', message); | ||
}, | ||
}, | ||
}; | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
.ai-modal { | ||
width: 400px; | ||
right: 0; | ||
left: 0; | ||
padding: var(--space-normal); | ||
bottom: 34px; | ||
position: absolute; | ||
span { | ||
font-size: var(--font-size-small); | ||
font-weight: var(--font-weight-medium); | ||
} | ||
p { | ||
color: var(--s-600); | ||
} | ||
label { | ||
margin-bottom: var(--space-smaller); | ||
} | ||
.status--filter { | ||
background-color: var(--color-background-light); | ||
border: 1px solid var(--color-border); | ||
font-size: var(--font-size-small); | ||
height: var(--space-large); | ||
padding: 0 var(--space-medium) 0 var(--space-small); | ||
} | ||
.modal-footer { | ||
gap: var(--space-smaller); | ||
} | ||
} | ||
</style> |
107 changes: 107 additions & 0 deletions
107
app/javascript/dashboard/components/widgets/AIAssistanceModal.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<template> | ||
<div class="column"> | ||
<woot-modal-header :header-title="headerTitle" /> | ||
<form class="row modal-content" @submit.prevent="applyText"> | ||
<div v-if="draftMessage" class="w-full"> | ||
<h4 class="sub-block-title margin-top-1 "> | ||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.DRAFT_TITLE') }} | ||
</h4> | ||
<p v-dompurify-html="formatMessage(draftMessage, false)" /> | ||
<h4 class="sub-block-title margin-top-1"> | ||
{{ | ||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.GENERATED_TITLE') | ||
}} | ||
</h4> | ||
</div> | ||
<div> | ||
<AILoader v-if="isGenerating" /> | ||
<p v-else v-dompurify-html="formatMessage(generatedContent, false)" /> | ||
</div> | ||
|
||
<div class="modal-footer justify-content-end w-full"> | ||
<woot-button variant="clear" @click.prevent="onClose"> | ||
{{ | ||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL') | ||
}} | ||
</woot-button> | ||
<woot-button :disabled="!generatedContent"> | ||
{{ | ||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY') | ||
}} | ||
</woot-button> | ||
</div> | ||
</form> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
import { mapGetters } from 'vuex'; | ||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | ||
import AILoader from './AILoader.vue'; | ||
import aiMixin from 'dashboard/mixins/aiMixin'; | ||
export default { | ||
components: { | ||
AILoader, | ||
}, | ||
mixins: [aiMixin, messageFormatterMixin], | ||
props: { | ||
aiOption: { | ||
type: String, | ||
required: true, | ||
}, | ||
}, | ||
data() { | ||
return { | ||
generatedContent: '', | ||
isGenerating: true, | ||
}; | ||
}, | ||
computed: { | ||
...mapGetters({ | ||
appIntegrations: 'integrations/getAppIntegrations', | ||
}), | ||
headerTitle() { | ||
const translationKey = this.aiOption?.toUpperCase(); | ||
return translationKey | ||
? this.$t(`INTEGRATION_SETTINGS.OPEN_AI.WITH_AI`, { | ||
option: this.$t( | ||
`INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.${translationKey}` | ||
), | ||
}) | ||
: ''; | ||
}, | ||
}, | ||
mounted() { | ||
this.generateAIContent(this.aiOption); | ||
}, | ||
methods: { | ||
onClose() { | ||
this.$emit('close'); | ||
}, | ||
async generateAIContent(type = 'rephrase') { | ||
this.isGenerating = true; | ||
this.generatedContent = await this.processEvent(type); | ||
this.isGenerating = false; | ||
}, | ||
applyText() { | ||
this.recordAnalytics(this.aiOption); | ||
this.$emit('apply-text', this.generatedContent); | ||
this.onClose(); | ||
}, | ||
}, | ||
}; | ||
</script> | ||
<style lang="scss" scoped> | ||
.modal-content { | ||
padding-top: var(--space-small); | ||
} | ||
.container { | ||
width: 100%; | ||
} | ||
</style> |
Oops, something went wrong.