feat: Ability to improve drafts in the editor using GPT integration (#6957)
ref: https://github.com/chatwoot/chatwoot/issues/6436 fixes: https://linear.app/chatwoot/issue/CW-1552/ability-to-rephrase-text-in-the-editor-using-gpt-integration --------- Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
23
app/javascript/dashboard/api/integrations/openapi.js
Normal file
23
app/javascript/dashboard/api/integrations/openapi.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class OpenAIAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('integrations', { accountScoped: true });
|
||||
}
|
||||
|
||||
processEvent({ name = 'rephrase', content, tone, hookId }) {
|
||||
return axios.post(`${this.url}/hooks/${hookId}/process_event`, {
|
||||
event: {
|
||||
name: name,
|
||||
data: {
|
||||
tone,
|
||||
content,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new OpenAIAPI();
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div v-if="isAIIntegrationEnabled" class="position-relative">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
|
||||
icon="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="isGenerating"
|
||||
size="small"
|
||||
@click="processText"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, clickaway],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isGenerating: false,
|
||||
showDropdown: false,
|
||||
activeTone: 'professional',
|
||||
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'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ appIntegrations: 'integrations/getAppIntegrations' }),
|
||||
isAIIntegrationEnabled() {
|
||||
return this.appIntegrations.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
);
|
||||
},
|
||||
hookId() {
|
||||
return this.appIntegrations.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
).hooks[0].id;
|
||||
},
|
||||
buttonText() {
|
||||
return this.isGenerating
|
||||
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING')
|
||||
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.appIntegrations.length) {
|
||||
this.$store.dispatch('integrations/get');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
this.showDropdown = !this.showDropdown;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showDropdown = false;
|
||||
},
|
||||
async processText() {
|
||||
this.isGenerating = true;
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
hookId: this.hookId,
|
||||
type: 'rephrase',
|
||||
content: this.message,
|
||||
tone: this.activeTone,
|
||||
});
|
||||
const {
|
||||
data: { message: generatedMessage },
|
||||
} = result;
|
||||
this.$emit('replace-text', generatedMessage || this.message);
|
||||
this.closeDropdown();
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
|
||||
} finally {
|
||||
this.isGenerating = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</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>
|
||||
@@ -91,6 +91,12 @@
|
||||
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
<AIAssistanceButton
|
||||
v-if="message"
|
||||
:conversation-id="conversationId"
|
||||
:message="message"
|
||||
@replace-text="replaceText"
|
||||
/>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
@@ -129,12 +135,13 @@ import {
|
||||
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
||||
} from 'shared/constants/messages';
|
||||
import VideoCallButton from '../VideoCallButton';
|
||||
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload, VideoCallButton },
|
||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
@@ -217,6 +224,10 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -303,6 +314,9 @@ export default {
|
||||
send_with_signature: !this.sendWithSignature,
|
||||
});
|
||||
},
|
||||
replaceText(text) {
|
||||
this.$emit('replace-text', text);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -120,8 +120,10 @@
|
||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||
:toggle-audio-recorder="toggleAudioRecorder"
|
||||
:toggle-emoji-picker="toggleEmojiPicker"
|
||||
:message="message"
|
||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||
@toggle-editor="toggleRichContentEditor"
|
||||
@replace-text="replaceText"
|
||||
/>
|
||||
<whatsapp-templates
|
||||
:inbox-id="inbox.id"
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
},
|
||||
"PLACEHOLDER": {
|
||||
"AGENT": "Search agents",
|
||||
"TEAM": "Search teams"
|
||||
"TEAM": "Search teams",
|
||||
"INPUT": "Search for agents"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,23 @@
|
||||
"JOIN_ERROR": "There was an error joining the call, please try again",
|
||||
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
|
||||
},
|
||||
"OPEN_AI": {
|
||||
"TITLE": "Improve With AI",
|
||||
"SUBTITLE": "An improved reply will be generated using AI, based on your current draft.",
|
||||
"TONE": {
|
||||
"TITLE": "Tone",
|
||||
"OPTIONS": {
|
||||
"PROFESSIONAL": "Professional",
|
||||
"FRIENDLY": "Friendly"
|
||||
}
|
||||
},
|
||||
"BUTTONS": {
|
||||
"GENERATE": "Generate",
|
||||
"GENERATING": "Generating...",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"GENERATE_ERROR": "There was an error processing the content, please try again"
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.NO_RESULTS.TEAM')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.INPUT_PLACEHOLDER')
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.INPUT')
|
||||
"
|
||||
@click="onClickAssignTeam"
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,9 @@ const state = {
|
||||
};
|
||||
|
||||
const isAValidAppIntegration = integration => {
|
||||
return ['dialogflow', 'dyte', 'google_translate'].includes(integration.id);
|
||||
return ['dialogflow', 'dyte', 'google_translate', 'openai'].includes(
|
||||
integration.id
|
||||
);
|
||||
};
|
||||
export const getters = {
|
||||
getIntegrations($state) {
|
||||
|
||||
Reference in New Issue
Block a user