feat: new Captain Editor (#13235)
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
import { useAI } from '../useAI';
|
||||
import {
|
||||
useStore,
|
||||
useStoreGetters,
|
||||
useMapGetter,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('vue-i18n');
|
||||
vi.mock('dashboard/api/integrations/openapi');
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
|
||||
const actual = await importOriginal();
|
||||
actual.default = {
|
||||
track: vi.fn(),
|
||||
};
|
||||
return actual;
|
||||
});
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
|
||||
OPEN_AI_EVENTS: {
|
||||
TEST_EVENT: 'open_ai_test_event',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAI', () => {
|
||||
const mockStore = {
|
||||
dispatch: vi.fn(),
|
||||
};
|
||||
|
||||
const mockGetters = {
|
||||
'integrations/getUIFlags': { value: { isFetching: false } },
|
||||
'draftMessages/get': { value: () => 'Draft message' },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useStore.mockReturnValue(mockStore);
|
||||
useStoreGetters.mockReturnValue(mockGetters);
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'integrations/getAppIntegrations': [],
|
||||
getSelectedChat: { id: '123' },
|
||||
'draftMessages/getReplyEditorMode': 'reply',
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
useI18n.mockReturnValue({ t: vi.fn() });
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', async () => {
|
||||
const { uiFlags, appIntegrations, currentChat, replyMode, draftMessage } =
|
||||
useAI();
|
||||
|
||||
expect(uiFlags.value).toEqual({ isFetching: false });
|
||||
expect(appIntegrations.value).toEqual([]);
|
||||
expect(currentChat.value).toEqual({ id: '123' });
|
||||
expect(replyMode.value).toBe('reply');
|
||||
expect(draftMessage.value).toBe('Draft message');
|
||||
});
|
||||
|
||||
it('fetches integrations if required', async () => {
|
||||
const { fetchIntegrationsIfRequired } = useAI();
|
||||
await fetchIntegrationsIfRequired();
|
||||
expect(mockStore.dispatch).toHaveBeenCalledWith('integrations/get');
|
||||
});
|
||||
|
||||
it('does not fetch integrations if already loaded', async () => {
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'integrations/getAppIntegrations': [{ id: 'openai' }],
|
||||
getSelectedChat: { id: '123' },
|
||||
'draftMessages/getReplyEditorMode': 'reply',
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
|
||||
const { fetchIntegrationsIfRequired } = useAI();
|
||||
await fetchIntegrationsIfRequired();
|
||||
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('records analytics correctly', async () => {
|
||||
// const mockTrack = analyticsHelper.track;
|
||||
const { recordAnalytics } = useAI();
|
||||
|
||||
await recordAnalytics('TEST_EVENT', { data: 'test' });
|
||||
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith('open_ai_test_event', {
|
||||
type: 'TEST_EVENT',
|
||||
data: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches label suggestions', async () => {
|
||||
OpenAPI.processEvent.mockResolvedValue({
|
||||
data: { message: 'label1, label2' },
|
||||
});
|
||||
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'integrations/getAppIntegrations': [
|
||||
{ id: 'openai', hooks: [{ id: 'hook1' }] },
|
||||
],
|
||||
getSelectedChat: { id: '123' },
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
|
||||
const { fetchLabelSuggestions } = useAI();
|
||||
const result = await fetchLabelSuggestions();
|
||||
|
||||
expect(OpenAPI.processEvent).toHaveBeenCalledWith({
|
||||
type: 'label_suggestion',
|
||||
hookId: 'hook1',
|
||||
conversationId: '123',
|
||||
});
|
||||
|
||||
expect(result).toEqual(['label1', 'label2']);
|
||||
});
|
||||
});
|
||||
213
app/javascript/dashboard/composables/spec/useCaptain.spec.js
Normal file
213
app/javascript/dashboard/composables/spec/useCaptain.spec.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useCaptain } from '../useCaptain';
|
||||
import {
|
||||
useFunctionGetter,
|
||||
useMapGetter,
|
||||
useStore,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import TasksAPI from 'dashboard/api/captain/tasks';
|
||||
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables/useAccount');
|
||||
vi.mock('dashboard/composables/useConfig');
|
||||
vi.mock('vue-i18n');
|
||||
vi.mock('dashboard/api/captain/tasks');
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
|
||||
const actual = await importOriginal();
|
||||
actual.default = {
|
||||
track: vi.fn(),
|
||||
};
|
||||
return actual;
|
||||
});
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
|
||||
OPEN_AI_EVENTS: {
|
||||
TEST_EVENT: 'open_ai_test_event',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useCaptain', () => {
|
||||
const mockStore = {
|
||||
dispatch: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useStore.mockReturnValue(mockStore);
|
||||
useFunctionGetter.mockReturnValue({ value: 'Draft message' });
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'accounts/getUIFlags': { isFetchingLimits: false },
|
||||
getSelectedChat: { id: '123' },
|
||||
'draftMessages/getReplyEditorMode': 'reply',
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
useI18n.mockReturnValue({ t: vi.fn() });
|
||||
useAccount.mockReturnValue({
|
||||
isCloudFeatureEnabled: vi.fn().mockReturnValue(true),
|
||||
currentAccount: { value: { limits: { captain: {} } } },
|
||||
});
|
||||
useConfig.mockReturnValue({
|
||||
isEnterprise: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', async () => {
|
||||
const { captainEnabled, captainTasksEnabled, currentChat, draftMessage } =
|
||||
useCaptain();
|
||||
|
||||
expect(captainEnabled.value).toBe(true);
|
||||
expect(captainTasksEnabled.value).toBe(true);
|
||||
expect(currentChat.value).toEqual({ id: '123' });
|
||||
expect(draftMessage.value).toBe('Draft message');
|
||||
});
|
||||
|
||||
it('records analytics correctly', async () => {
|
||||
const { recordAnalytics } = useCaptain();
|
||||
|
||||
await recordAnalytics('TEST_EVENT', { data: 'test' });
|
||||
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith('open_ai_test_event', {
|
||||
type: 'TEST_EVENT',
|
||||
data: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets label suggestions', async () => {
|
||||
TasksAPI.labelSuggestion.mockResolvedValue({
|
||||
data: { message: 'label1, label2' },
|
||||
});
|
||||
|
||||
const { getLabelSuggestions } = useCaptain();
|
||||
const result = await getLabelSuggestions();
|
||||
|
||||
expect(TasksAPI.labelSuggestion).toHaveBeenCalledWith('123');
|
||||
expect(result).toEqual(['label1', 'label2']);
|
||||
});
|
||||
|
||||
it('rewrites content', async () => {
|
||||
TasksAPI.rewrite.mockResolvedValue({
|
||||
data: { message: 'Rewritten content', follow_up_context: { id: 'ctx1' } },
|
||||
});
|
||||
|
||||
const { rewriteContent } = useCaptain();
|
||||
const result = await rewriteContent('Original content', 'improve', {});
|
||||
|
||||
expect(TasksAPI.rewrite).toHaveBeenCalledWith(
|
||||
{
|
||||
content: 'Original content',
|
||||
operation: 'improve',
|
||||
conversationId: '123',
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(result).toEqual({
|
||||
message: 'Rewritten content',
|
||||
followUpContext: { id: 'ctx1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('summarizes conversation', async () => {
|
||||
TasksAPI.summarize.mockResolvedValue({
|
||||
data: { message: 'Summary', follow_up_context: { id: 'ctx2' } },
|
||||
});
|
||||
|
||||
const { summarizeConversation } = useCaptain();
|
||||
const result = await summarizeConversation({});
|
||||
|
||||
expect(TasksAPI.summarize).toHaveBeenCalledWith('123', undefined);
|
||||
expect(result).toEqual({
|
||||
message: 'Summary',
|
||||
followUpContext: { id: 'ctx2' },
|
||||
});
|
||||
});
|
||||
|
||||
it('gets reply suggestion', async () => {
|
||||
TasksAPI.replySuggestion.mockResolvedValue({
|
||||
data: { message: 'Reply suggestion', follow_up_context: { id: 'ctx3' } },
|
||||
});
|
||||
|
||||
const { getReplySuggestion } = useCaptain();
|
||||
const result = await getReplySuggestion({});
|
||||
|
||||
expect(TasksAPI.replySuggestion).toHaveBeenCalledWith('123', undefined);
|
||||
expect(result).toEqual({
|
||||
message: 'Reply suggestion',
|
||||
followUpContext: { id: 'ctx3' },
|
||||
});
|
||||
});
|
||||
|
||||
it('sends follow-up message', async () => {
|
||||
TasksAPI.followUp.mockResolvedValue({
|
||||
data: {
|
||||
message: 'Follow-up response',
|
||||
follow_up_context: { id: 'ctx4' },
|
||||
},
|
||||
});
|
||||
|
||||
const { followUp } = useCaptain();
|
||||
const result = await followUp({
|
||||
followUpContext: { id: 'ctx3' },
|
||||
message: 'Make it shorter',
|
||||
});
|
||||
|
||||
expect(TasksAPI.followUp).toHaveBeenCalledWith(
|
||||
{
|
||||
followUpContext: { id: 'ctx3' },
|
||||
message: 'Make it shorter',
|
||||
conversationId: '123',
|
||||
},
|
||||
undefined
|
||||
);
|
||||
expect(result).toEqual({
|
||||
message: 'Follow-up response',
|
||||
followUpContext: { id: 'ctx4' },
|
||||
});
|
||||
});
|
||||
|
||||
it('processes event and routes to correct method', async () => {
|
||||
TasksAPI.summarize.mockResolvedValue({
|
||||
data: { message: 'Summary' },
|
||||
});
|
||||
TasksAPI.replySuggestion.mockResolvedValue({
|
||||
data: { message: 'Reply' },
|
||||
});
|
||||
TasksAPI.rewrite.mockResolvedValue({
|
||||
data: { message: 'Rewritten' },
|
||||
});
|
||||
|
||||
const { processEvent } = useCaptain();
|
||||
|
||||
// Test summarize
|
||||
await processEvent('summarize', '', {});
|
||||
expect(TasksAPI.summarize).toHaveBeenCalled();
|
||||
|
||||
// Test reply_suggestion
|
||||
await processEvent('reply_suggestion', '', {});
|
||||
expect(TasksAPI.replySuggestion).toHaveBeenCalled();
|
||||
|
||||
// Test rewrite (improve)
|
||||
await processEvent('improve', 'content', {});
|
||||
expect(TasksAPI.rewrite).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns empty array when no conversation ID for label suggestions', async () => {
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const mockValues = {
|
||||
'accounts/getUIFlags': { isFetchingLimits: false },
|
||||
getSelectedChat: { id: null },
|
||||
'draftMessages/getReplyEditorMode': 'reply',
|
||||
};
|
||||
return { value: mockValues[getter] };
|
||||
});
|
||||
|
||||
const { getLabelSuggestions } = useCaptain();
|
||||
const result = await getLabelSuggestions();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(TasksAPI.labelSuggestion).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user