feat: Rewrite aiMixin to a composable (#9955)

This PR will replace the usage of aiMixin with the useAI composable.

Fixes https://linear.app/chatwoot/issue/CW-3443/rewrite-aimixin-mixin-to-a-composable

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
Sivin Varghese
2024-08-22 13:58:51 +05:30
committed by GitHub
parent c63a6ed8ec
commit d19a9c38d7
11 changed files with 372 additions and 223 deletions

View File

@@ -1,112 +0,0 @@
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events';
import OpenAPI from '../api/integrations/openapi';
export default {
mounted() {
this.fetchIntegrationsIfRequired();
},
computed: {
...mapGetters({
uiFlags: 'integrations/getUIFlags',
appIntegrations: 'integrations/getAppIntegrations',
currentChat: 'getSelectedChat',
replyMode: 'draftMessages/getReplyEditorMode',
}),
aiIntegration() {
return this.appIntegrations.find(
integration => integration.id === 'openai' && !!integration.hooks.length
)?.hooks[0];
},
isAIIntegrationEnabled() {
return !!this.aiIntegration;
},
isLabelSuggestionFeatureEnabled() {
if (this.aiIntegration) {
const { settings = {} } = this.aiIntegration || {};
return settings.label_suggestion;
}
return false;
},
isFetchingAppIntegrations() {
return this.uiFlags.isFetching;
},
hookId() {
return this.aiIntegration.id;
},
draftMessage() {
return this.$store.getters['draftMessages/get'](this.draftKey);
},
draftKey() {
return `draft-${this.conversationId}-${this.replyMode}`;
},
conversationId() {
return this.currentChat?.id;
},
},
methods: {
async fetchIntegrationsIfRequired() {
if (!this.appIntegrations.length) {
await this.$store.dispatch('integrations/get');
}
},
async recordAnalytics(type, payload) {
const event = OPEN_AI_EVENTS[type.toUpperCase()];
if (event) {
this.$track(event, {
type,
...payload,
});
}
},
async fetchLabelSuggestions({ conversationId }) {
if (!conversationId) return [];
try {
const result = await OpenAPI.processEvent({
type: 'label_suggestion',
hookId: this.hookId,
conversationId: conversationId,
});
const {
data: { message: labels },
} = result;
return this.cleanLabels(labels);
} catch (error) {
return [];
}
},
cleanLabels(labels) {
return labels
.toLowerCase() // Set it to lowercase
.split(',') // split the string into an array
.filter(label => label.trim()) // remove any empty strings
.map(label => label.trim()) // trim the words
.filter((label, index, self) => self.indexOf(label) === index); // remove any duplicates
},
async processEvent(type = 'rephrase') {
try {
const result = await OpenAPI.processEvent({
hookId: this.hookId,
type,
content: this.draftMessage,
conversationId: this.conversationId,
});
const {
data: { message: generatedMessage },
} = result;
return generatedMessage;
} catch (error) {
const errorData = error.response.data.error;
const errorMessage =
errorData?.error?.message ||
this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR');
useAlert(errorMessage);
return '';
}
},
},
};

View File

@@ -1,95 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import aiMixin from '../aiMixin';
import Vuex from 'vuex';
import OpenAPI from '../../api/integrations/openapi';
import { LocalStorage } from '../../../shared/helpers/localStorage';
vi.mock('../../api/integrations/openapi');
vi.mock('../../../shared/helpers/localStorage');
const localVue = createLocalVue();
localVue.use(Vuex);
describe('aiMixin', () => {
let wrapper;
let getters;
let emptyGetters;
let component;
let actions;
beforeEach(() => {
OpenAPI.processEvent = vi.fn();
LocalStorage.set = vi.fn();
LocalStorage.get = vi.fn();
actions = {
['integrations/get']: vi.fn(),
};
getters = {
['integrations/getAppIntegrations']: () => [
{
id: 'openai',
hooks: [{ id: 'hook1' }],
},
],
};
component = {
render() {},
title: 'TestComponent',
mixins: [aiMixin],
};
wrapper = shallowMount(component, {
store: new Vuex.Store({
getters: getters,
actions,
}),
localVue,
});
emptyGetters = {
['integrations/getAppIntegrations']: () => [],
};
});
it('fetches integrations if required', async () => {
wrapper = shallowMount(component, {
store: new Vuex.Store({
getters: emptyGetters,
actions,
}),
localVue,
});
const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
await wrapper.vm.fetchIntegrationsIfRequired();
expect(dispatchSpy).toHaveBeenCalledWith('integrations/get');
});
it('does not fetch integrations', async () => {
const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
await wrapper.vm.fetchIntegrationsIfRequired();
expect(dispatchSpy).not.toHaveBeenCalledWith('integrations/get');
expect(wrapper.vm.isAIIntegrationEnabled).toBeTruthy();
});
it('fetches label suggestions', async () => {
const processEventSpy = vi.spyOn(OpenAPI, 'processEvent');
await wrapper.vm.fetchLabelSuggestions({
conversationId: '123',
});
expect(processEventSpy).toHaveBeenCalledWith({
type: 'label_suggestion',
hookId: 'hook1',
conversationId: '123',
});
});
it('cleans labels', () => {
const labels = 'label1, label2, label1';
expect(wrapper.vm.cleanLabels(labels)).toEqual(['label1', 'label2']);
});
});