diff --git a/.circleci/config.yml b/.circleci/config.yml index ff82abd24..09bd5191d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,7 +76,7 @@ jobs: bundle install - node/install: - node-version: '24.12' + node-version: '24.13' - node/install-pnpm - node/install-packages: pkg-manager: pnpm @@ -117,7 +117,7 @@ jobs: steps: - checkout - node/install: - node-version: '24.12' + node-version: '24.13' - node/install-pnpm - node/install-packages: pkg-manager: pnpm @@ -148,7 +148,7 @@ jobs: steps: - checkout - node/install: - node-version: '24.12' + node-version: '24.13' - node/install-pnpm - node/install-packages: pkg-manager: pnpm diff --git a/.devcontainer/docker-compose.base.yml b/.devcontainer/docker-compose.base.yml index b7db87097..375742ff7 100644 --- a/.devcontainer/docker-compose.base.yml +++ b/.devcontainer/docker-compose.base.yml @@ -10,7 +10,7 @@ services: dockerfile: .devcontainer/Dockerfile.base args: VARIANT: 'ubuntu-22.04' - NODE_VERSION: '24.12.0' + NODE_VERSION: '24.13.0' RUBY_VERSION: '3.4.4' # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. USER_UID: '1000' diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 3618b0681..d696f99cc 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -11,7 +11,7 @@ services: dockerfile: .devcontainer/Dockerfile args: VARIANT: 'ubuntu-22.04' - NODE_VERSION: '24.12.0' + NODE_VERSION: '24.13.0' RUBY_VERSION: '3.4.4' # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. USER_UID: '1000' diff --git a/.nvmrc b/.nvmrc index 1e4f3920b..cf2efde81 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.12.0 \ No newline at end of file +24.13.0 \ No newline at end of file diff --git a/VERSION_CW b/VERSION_CW index dad10c76d..ad96464c4 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -4.9.2 +4.10.1 diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb index f5bed6c34..0cde5f5c1 100644 --- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb +++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb @@ -50,3 +50,5 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base @current_page = params[:page] || 1 end end + +Api::V1::Accounts::CsatSurveyResponsesController.prepend_mod_with('Api::V1::Accounts::CsatSurveyResponsesController') 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-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index a0e83973d..fb5e3ddcf 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -15,7 +15,6 @@ import Button from 'dashboard/components-next/button/Button.vue'; import SidebarGroup from './SidebarGroup.vue'; import SidebarProfileMenu from './SidebarProfileMenu.vue'; import SidebarChangelogCard from './SidebarChangelogCard.vue'; -import YearInReviewBanner from '../year-in-review/YearInReviewBanner.vue'; import ChannelLeaf from './ChannelLeaf.vue'; import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue'; import Logo from 'next/icon/Logo.vue'; @@ -661,7 +660,6 @@ const menuItems = computed(() => {
- diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue index 3dad3c76b..36aad75d0 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenu.vue @@ -1,13 +1,11 @@