From 6a482926b4abca4792bd228001021ce190ae8187 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 21 Jan 2026 13:39:07 +0530 Subject: [PATCH] feat: new Captain Editor (#13235) Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Co-authored-by: Vishnu Narayanan Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin Co-authored-by: aakashb95 --- .../super_admin/app_configs_controller.rb | 3 +- app/helpers/super_admin/features.yml | 13 +- app/javascript/dashboard/api/captain/tasks.js | 107 ++++++ .../dashboard/api/integrations/openapi.js | 81 ----- .../dashboard/assets/scss/_next-colors.scss | 26 ++ .../components/widgets/AIAssistanceButton.vue | 160 --------- .../widgets/AIAssistanceCTAButton.vue | 103 ------ .../components/widgets/AIAssistanceModal.vue | 118 ------- .../components/widgets/AICTAModal.vue | 130 ------- .../components/widgets/AttachmentsPreview.vue | 4 +- .../widgets/WootWriter/CopilotEditor.vue | 253 ++++++++++++++ .../widgets/WootWriter/CopilotMenuBar.vue | 259 ++++++++++++++ .../WootWriter/CopilotReplyBottomPanel.vue | 51 +++ .../components/widgets/WootWriter/Editor.vue | 73 +++- .../widgets/WootWriter/EditorModeToggle.vue | 20 +- .../widgets/WootWriter/ReplyBottomPanel.vue | 11 +- .../widgets/WootWriter/ReplyTopPanel.vue | 79 ++++- .../conversation/CopilotEditorSection.vue | 99 ++++++ .../MessageSignatureMissingAlert.vue | 2 +- .../widgets/conversation/MessagesView.vue | 36 +- .../widgets/conversation/ReplyBox.vue | 305 ++++++++++------ .../conversation/LabelSuggestion.vue | 8 +- .../conversation/copilot/CaptainLoader.vue | 33 ++ .../spec/useConversationHotKeys.spec.js | 12 +- .../commands/useConversationHotKeys.js | 29 +- .../dashboard/composables/spec/useAI.spec.js | 122 ------- .../composables/spec/useCaptain.spec.js | 213 ++++++++++++ app/javascript/dashboard/composables/useAI.js | 203 ----------- .../dashboard/composables/useCaptain.js | 226 +++++++++++- .../dashboard/composables/useCopilotReply.js | 162 +++++++++ .../dashboard/composables/utils/useKbd.js | 21 +- app/javascript/dashboard/constants/editor.js | 27 +- app/javascript/dashboard/featureFlags.js | 1 + .../helper/AnalyticsHelper/events.js | 1 + .../dashboard/helper/editorHelper.js | 11 +- .../helper/specs/editorHelper.spec.js | 56 --- .../i18n/locale/en/conversation.json | 3 +- .../dashboard/i18n/locale/en/general.json | 2 + .../i18n/locale/en/integrations.json | 24 +- app/javascript/shared/constants/openai.js | 11 - app/models/integrations/hook.rb | 11 +- app/policies/captain/tasks_policy.rb | 21 ++ .../conversation_llm_formatter.rb | 32 +- config/features.yml | 3 + config/locales/en.yml | 3 + config/routes.rb | 7 + ...ble_captain_tasks_for_existing_accounts.rb | 12 + db/schema.rb | 2 +- .../v1/accounts/captain/tasks_controller.rb | 71 ++++ enterprise/config/premium_features.yml | 2 +- .../enterprise/captain/base_task_service.rb | 32 ++ .../integrations/openai_processor_service.rb | 82 ----- lib/captain/base_task_service.rb | 181 ++++++++++ lib/captain/follow_up_service.rb | 106 ++++++ lib/captain/label_suggestion_service.rb | 93 +++++ lib/captain/reply_suggestion_service.rb | 40 +++ lib/captain/rewrite_service.rb | 59 ++++ lib/captain/summary_service.rb | 19 + lib/integrations/llm_base_service.rb | 3 +- .../fix_spelling_grammar.liquid | 17 + .../openai/openai_prompts/improve.liquid | 43 +++ .../openai_prompts/label_suggestion.liquid | 2 +- .../openai/openai_prompts/reply.liquid | 35 ++ .../openai/openai_prompts/reply.txt | 1 - .../openai/openai_prompts/summary.liquid | 10 +- .../openai/openai_prompts/summary.txt | 1 - .../openai/openai_prompts/tone_rewrite.liquid | 35 ++ lib/integrations/openai/processor_service.rb | 138 -------- lib/llm/config.rb | 3 +- package.json | 5 +- pnpm-lock.yaml | 20 +- .../lib/captain/base_task_service_spec.rb | 169 +++++++++ .../openai/processor_service_spec.rb | 120 ------- spec/lib/captain/base_task_service_spec.rb | 325 ++++++++++++++++++ spec/lib/captain/follow_up_service_spec.rb | 164 +++++++++ .../captain/label_suggestion_service_spec.rb | 169 +++++++++ .../captain/reply_suggestion_service_spec.rb | 92 +++++ spec/lib/captain/rewrite_service_spec.rb | 166 +++++++++ spec/lib/captain/summary_service_spec.rb | 55 +++ .../openai/processor_service_spec.rb | 201 ----------- spec/models/integrations/hook_spec.rb | 21 -- tailwind.config.js | 1 + theme/colors.js | 15 + 83 files changed, 3887 insertions(+), 1798 deletions(-) create mode 100644 app/javascript/dashboard/api/captain/tasks.js delete mode 100644 app/javascript/dashboard/api/integrations/openapi.js delete mode 100644 app/javascript/dashboard/components/widgets/AIAssistanceButton.vue delete mode 100644 app/javascript/dashboard/components/widgets/AIAssistanceCTAButton.vue delete mode 100644 app/javascript/dashboard/components/widgets/AIAssistanceModal.vue delete mode 100644 app/javascript/dashboard/components/widgets/AICTAModal.vue create mode 100644 app/javascript/dashboard/components/widgets/WootWriter/CopilotEditor.vue create mode 100644 app/javascript/dashboard/components/widgets/WootWriter/CopilotMenuBar.vue create mode 100644 app/javascript/dashboard/components/widgets/WootWriter/CopilotReplyBottomPanel.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/CopilotEditorSection.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/copilot/CaptainLoader.vue delete mode 100644 app/javascript/dashboard/composables/spec/useAI.spec.js create mode 100644 app/javascript/dashboard/composables/spec/useCaptain.spec.js delete mode 100644 app/javascript/dashboard/composables/useAI.js create mode 100644 app/javascript/dashboard/composables/useCopilotReply.js delete mode 100644 app/javascript/shared/constants/openai.js create mode 100644 app/policies/captain/tasks_policy.rb create mode 100644 db/migrate/20260120121402_enable_captain_tasks_for_existing_accounts.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/captain/tasks_controller.rb create mode 100644 enterprise/lib/enterprise/captain/base_task_service.rb delete mode 100644 enterprise/lib/enterprise/integrations/openai_processor_service.rb create mode 100644 lib/captain/base_task_service.rb create mode 100644 lib/captain/follow_up_service.rb create mode 100644 lib/captain/label_suggestion_service.rb create mode 100644 lib/captain/reply_suggestion_service.rb create mode 100644 lib/captain/rewrite_service.rb create mode 100644 lib/captain/summary_service.rb create mode 100644 lib/integrations/openai/openai_prompts/fix_spelling_grammar.liquid create mode 100644 lib/integrations/openai/openai_prompts/improve.liquid rename enterprise/lib/enterprise/integrations/openai_prompts/label_suggestion.txt => lib/integrations/openai/openai_prompts/label_suggestion.liquid (88%) create mode 100644 lib/integrations/openai/openai_prompts/reply.liquid delete mode 100644 lib/integrations/openai/openai_prompts/reply.txt rename enterprise/lib/enterprise/integrations/openai_prompts/summary.txt => lib/integrations/openai/openai_prompts/summary.liquid (93%) delete mode 100644 lib/integrations/openai/openai_prompts/summary.txt create mode 100644 lib/integrations/openai/openai_prompts/tone_rewrite.liquid delete mode 100644 lib/integrations/openai/processor_service.rb create mode 100644 spec/enterprise/lib/captain/base_task_service_spec.rb delete mode 100644 spec/enterprise/lib/integrations/openai/processor_service_spec.rb create mode 100644 spec/lib/captain/base_task_service_spec.rb create mode 100644 spec/lib/captain/follow_up_service_spec.rb create mode 100644 spec/lib/captain/label_suggestion_service_spec.rb create mode 100644 spec/lib/captain/reply_suggestion_service_spec.rb create mode 100644 spec/lib/captain/rewrite_service_spec.rb create mode 100644 spec/lib/captain/summary_service_spec.rb delete mode 100644 spec/lib/integrations/openai/processor_service_spec.rb diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index ec51305b5..e9d27c67a 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -49,7 +49,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'tiktok' => %w[TIKTOK_APP_ID TIKTOK_APP_SECRET], 'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION], 'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET], - 'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN] + 'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN], + 'captain' => %w[CAPTAIN_OPEN_AI_API_KEY CAPTAIN_OPEN_AI_MODEL CAPTAIN_OPEN_AI_ENDPOINT] } @allowed_configs = mapping.fetch( diff --git a/app/helpers/super_admin/features.yml b/app/helpers/super_admin/features.yml index 34c7a8138..f21a97f78 100644 --- a/app/helpers/super_admin/features.yml +++ b/app/helpers/super_admin/features.yml @@ -2,13 +2,6 @@ # No need to replicate the same values in two places # ------- Premium Features ------- # -captain: - name: 'Captain' - description: 'Enable AI-powered conversations with your customers.' - enabled: <%= (ChatwootHub.pricing_plan != 'community') %> - icon: 'icon-captain' - config_key: 'captain' - enterprise: true saml: name: 'SAML SSO' description: 'Configuration for controlling SAML Single Sign-On availability' @@ -48,6 +41,12 @@ help_center: description: 'Allow agents to create help center articles and publish them in a portal.' enabled: true icon: 'icon-book-2-line' +captain: + name: 'Captain' + description: 'Enable AI-powered conversations with your customers.' + enabled: true + icon: 'icon-captain' + config_key: 'captain' # ------- Communication Channels ------- # live_chat: diff --git a/app/javascript/dashboard/api/captain/tasks.js b/app/javascript/dashboard/api/captain/tasks.js new file mode 100644 index 000000000..1b5a38335 --- /dev/null +++ b/app/javascript/dashboard/api/captain/tasks.js @@ -0,0 +1,107 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +/** + * A client for the Captain Tasks API. + * @extends ApiClient + */ +class TasksAPI extends ApiClient { + /** + * Creates a new TasksAPI instance. + */ + constructor() { + super('captain/tasks', { accountScoped: true }); + } + + /** + * Rewrites content with a specific operation. + * @param {Object} options - The rewrite options. + * @param {string} options.content - The content to rewrite. + * @param {string} options.operation - The rewrite operation (fix_spelling_grammar, casual, professional, etc). + * @param {string} [options.conversationId] - The conversation ID for context (required for 'improve'). + * @param {AbortSignal} [signal] - AbortSignal to cancel the request. + * @returns {Promise} A promise that resolves with the rewritten content. + */ + rewrite({ content, operation, conversationId }, signal) { + return axios.post( + `${this.url}/rewrite`, + { + content, + operation, + conversation_display_id: conversationId, + }, + { signal } + ); + } + + /** + * Summarizes a conversation. + * @param {string} conversationId - The conversation ID to summarize. + * @param {AbortSignal} [signal] - AbortSignal to cancel the request. + * @returns {Promise} A promise that resolves with the summary. + */ + summarize(conversationId, signal) { + return axios.post( + `${this.url}/summarize`, + { + conversation_display_id: conversationId, + }, + { signal } + ); + } + + /** + * Gets a reply suggestion for a conversation. + * @param {string} conversationId - The conversation ID. + * @param {AbortSignal} [signal] - AbortSignal to cancel the request. + * @returns {Promise} A promise that resolves with the reply suggestion. + */ + replySuggestion(conversationId, signal) { + return axios.post( + `${this.url}/reply_suggestion`, + { + conversation_display_id: conversationId, + }, + { signal } + ); + } + + /** + * Gets label suggestions for a conversation. + * @param {string} conversationId - The conversation ID. + * @param {AbortSignal} [signal] - AbortSignal to cancel the request. + * @returns {Promise} A promise that resolves with label suggestions. + */ + labelSuggestion(conversationId, signal) { + return axios.post( + `${this.url}/label_suggestion`, + { + conversation_display_id: conversationId, + }, + { signal } + ); + } + + /** + * Sends a follow-up message to continue refining a previous task result. + * @param {Object} options - The follow-up options. + * @param {Object} options.followUpContext - The follow-up context from a previous task. + * @param {string} options.message - The follow-up message/request from the user. + * @param {string} [options.conversationId] - The conversation ID for Langfuse session tracking. + * @param {AbortSignal} [signal] - AbortSignal to cancel the request. + * @returns {Promise} A promise that resolves with the follow-up response and updated follow-up context. + */ + followUp({ followUpContext, message, conversationId }, signal) { + return axios.post( + `${this.url}/follow_up`, + { + follow_up_context: followUpContext, + message, + conversation_display_id: conversationId, + }, + { signal } + ); + } +} + +export default new TasksAPI(); diff --git a/app/javascript/dashboard/api/integrations/openapi.js b/app/javascript/dashboard/api/integrations/openapi.js deleted file mode 100644 index 3fcf241ee..000000000 --- a/app/javascript/dashboard/api/integrations/openapi.js +++ /dev/null @@ -1,81 +0,0 @@ -/* global axios */ - -import ApiClient from '../ApiClient'; - -/** - * Represents the data object for a OpenAI hook. - * @typedef {Object} ConversationMessageData - * @property {string} [tone] - The tone of the message. - * @property {string} [content] - The content of the message. - * @property {string} [conversation_display_id] - The display ID of the conversation (optional). - */ - -/** - * A client for the OpenAI API. - * @extends ApiClient - */ -class OpenAIAPI extends ApiClient { - /** - * Creates a new OpenAIAPI instance. - */ - constructor() { - super('integrations', { accountScoped: true }); - - /** - * The conversation events supported by the API. - * @type {string[]} - */ - this.conversation_events = [ - 'summarize', - 'reply_suggestion', - 'label_suggestion', - ]; - - /** - * The message events supported by the API. - * @type {string[]} - */ - this.message_events = ['rephrase']; - } - - /** - * Processes an event using the OpenAI API. - * @param {Object} options - The options for the event. - * @param {string} [options.type='rephrase'] - The type of event to process. - * @param {string} [options.content] - The content of the event. - * @param {string} [options.tone] - The tone of the event. - * @param {string} [options.conversationId] - The ID of the conversation to process the event for. - * @param {string} options.hookId - The ID of the hook to use for processing the event. - * @returns {Promise} A promise that resolves with the result of the event processing. - */ - processEvent({ type = 'rephrase', content, tone, conversationId, hookId }) { - /** - * @type {ConversationMessageData} - */ - let data = { - tone, - content, - }; - - // Always include conversation_display_id when available for session tracking - if (conversationId) { - data.conversation_display_id = conversationId; - } - - // For conversation-level events, only send conversation_display_id - if (this.conversation_events.includes(type)) { - data = { - conversation_display_id: conversationId, - }; - } - - return axios.post(`${this.url}/hooks/${hookId}/process_event`, { - event: { - name: type, - data, - }, - }); - } -} - -export default new OpenAIAPI(); diff --git a/app/javascript/dashboard/assets/scss/_next-colors.scss b/app/javascript/dashboard/assets/scss/_next-colors.scss index f23c01d42..4d7975a32 100644 --- a/app/javascript/dashboard/assets/scss/_next-colors.scss +++ b/app/javascript/dashboard/assets/scss/_next-colors.scss @@ -94,6 +94,19 @@ --gray-11: 100 100 100; --gray-12: 32 32 32; + --violet-1: 253 252 254; + --violet-2: 250 248 255; + --violet-3: 244 240 254; + --violet-4: 235 228 255; + --violet-5: 225 217 255; + --violet-6: 212 202 254; + --violet-7: 194 178 248; + --violet-8: 169 153 236; + --violet-9: 110 86 207; + --violet-10: 100 84 196; + --violet-11: 101 85 183; + --violet-12: 47 38 95; + --background-color: 253 253 253; --text-blue: 8 109 224; --border-container: 236 236 236; @@ -209,6 +222,19 @@ --gray-11: 180 180 180; --gray-12: 238 238 238; + --violet-1: 20 17 31; + --violet-2: 27 21 37; + --violet-3: 41 31 67; + --violet-4: 50 37 85; + --violet-5: 60 46 105; + --violet-6: 71 56 135; + --violet-7: 86 70 151; + --violet-8: 110 86 171; + --violet-9: 110 86 207; + --violet-10: 125 109 217; + --violet-11: 169 153 236; + --violet-12: 226 221 254; + --background-color: 18 18 19; --border-strong: 52 52 52; --border-weak: 38 38 42; diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue deleted file mode 100644 index f7a94fb85..000000000 --- a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceCTAButton.vue b/app/javascript/dashboard/components/widgets/AIAssistanceCTAButton.vue deleted file mode 100644 index 6fbdfe6e7..000000000 --- a/app/javascript/dashboard/components/widgets/AIAssistanceCTAButton.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceModal.vue b/app/javascript/dashboard/components/widgets/AIAssistanceModal.vue deleted file mode 100644 index 04bba8f59..000000000 --- a/app/javascript/dashboard/components/widgets/AIAssistanceModal.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/components/widgets/AICTAModal.vue b/app/javascript/dashboard/components/widgets/AICTAModal.vue deleted file mode 100644 index 13e201326..000000000 --- a/app/javascript/dashboard/components/widgets/AICTAModal.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue index 75afb5263..c924dd65d 100644 --- a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue +++ b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue @@ -46,11 +46,11 @@ const fileName = file => {