From 4f4ef0389b864620fc121382bb50be6ec71e9006 Mon Sep 17 00:00:00 2001
From: Pranav
Date: Thu, 15 May 2025 17:37:04 -0700
Subject: [PATCH 1/6] feat: Add support for persistent copilot threads and
messages (#11489)
The agents can see the previous conversations with the copilot if needed
with this change. We would have to cleanup the data after a while. For
now, that is not considered.
This PR adds:
- A new model for copilot_threads (intentionally named thread instead of
conversation to avoid confusion), copilot_messages
- Add the controller to fetch previous threads and messages.
---
config/routes.rb | 5 +-
.../20250512231036_create_copilot_threads.rb | 14 ++++++
.../20250512231037_create_copilot_messages.rb | 13 +++++
db/schema.rb | 27 +++++++++-
.../captain/copilot_messages_controller.rb | 25 ++++++++++
.../captain/copilot_threads_controller.rb | 19 +++++++
enterprise/app/models/copilot_message.rb | 27 ++++++++++
enterprise/app/models/copilot_thread.rb | 26 ++++++++++
.../app/models/enterprise/concerns/account.rb | 2 +
.../app/models/enterprise/concerns/user.rb | 2 +
.../copilot_messages/index.json.jbuilder | 8 +++
.../copilot_threads/index.json.jbuilder | 12 +++++
.../copilot_messages_controller_spec.rb | 33 ++++++++++++
.../copilot_threads_controller_spec.rb | 50 +++++++++++++++++++
spec/factories/captain/copilot_message.rb | 9 ++++
spec/factories/captain/copilot_thread.rb | 8 +++
16 files changed, 278 insertions(+), 2 deletions(-)
create mode 100644 db/migrate/20250512231036_create_copilot_threads.rb
create mode 100644 db/migrate/20250512231037_create_copilot_messages.rb
create mode 100644 enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb
create mode 100644 enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb
create mode 100644 enterprise/app/models/copilot_message.rb
create mode 100644 enterprise/app/models/copilot_thread.rb
create mode 100644 enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder
create mode 100644 enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder
create mode 100644 spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb
create mode 100644 spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb
create mode 100644 spec/factories/captain/copilot_message.rb
create mode 100644 spec/factories/captain/copilot_thread.rb
diff --git a/config/routes.rb b/config/routes.rb
index c623ff053..ee6ec5e8a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,9 +58,12 @@ Rails.application.routes.draw do
end
resources :inboxes, only: [:index, :create, :destroy], param: :inbox_id
end
- resources :documents, only: [:index, :show, :create, :destroy]
resources :assistant_responses
resources :bulk_actions, only: [:create]
+ resources :copilot_threads, only: [:index] do
+ resources :copilot_messages, only: [:index]
+ end
+ resources :documents, only: [:index, :show, :create, :destroy]
end
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
delete :avatar, on: :member
diff --git a/db/migrate/20250512231036_create_copilot_threads.rb b/db/migrate/20250512231036_create_copilot_threads.rb
new file mode 100644
index 000000000..68686e066
--- /dev/null
+++ b/db/migrate/20250512231036_create_copilot_threads.rb
@@ -0,0 +1,14 @@
+class CreateCopilotThreads < ActiveRecord::Migration[7.0]
+ def change
+ create_table :copilot_threads do |t|
+ t.string :title, null: false
+ t.references :user, null: false, index: true
+ t.references :account, null: false, index: true
+ t.uuid :uuid, null: false, default: 'gen_random_uuid()'
+
+ t.timestamps
+ end
+
+ add_index :copilot_threads, :uuid, unique: true
+ end
+end
diff --git a/db/migrate/20250512231037_create_copilot_messages.rb b/db/migrate/20250512231037_create_copilot_messages.rb
new file mode 100644
index 000000000..fd03cc9f8
--- /dev/null
+++ b/db/migrate/20250512231037_create_copilot_messages.rb
@@ -0,0 +1,13 @@
+class CreateCopilotMessages < ActiveRecord::Migration[7.0]
+ def change
+ create_table :copilot_messages do |t|
+ t.references :copilot_thread, null: false, index: true
+ t.references :user, null: false, index: true
+ t.references :account, null: false, index: true
+ t.string :message_type, null: false
+ t.jsonb :message, null: false, default: {}
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d858899ab..452354d49 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2025_04_21_085134) do
+ActiveRecord::Schema[7.0].define(version: 2025_05_12_231037) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -575,6 +575,31 @@ ActiveRecord::Schema[7.0].define(version: 2025_04_21_085134) do
t.index ["waiting_since"], name: "index_conversations_on_waiting_since"
end
+ create_table "copilot_messages", force: :cascade do |t|
+ t.bigint "copilot_thread_id", null: false
+ t.bigint "user_id", null: false
+ t.bigint "account_id", null: false
+ t.string "message_type", null: false
+ t.jsonb "message", default: {}, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_copilot_messages_on_account_id"
+ t.index ["copilot_thread_id"], name: "index_copilot_messages_on_copilot_thread_id"
+ t.index ["user_id"], name: "index_copilot_messages_on_user_id"
+ end
+
+ create_table "copilot_threads", force: :cascade do |t|
+ t.string "title", null: false
+ t.bigint "user_id", null: false
+ t.bigint "account_id", null: false
+ t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_copilot_threads_on_account_id"
+ t.index ["user_id"], name: "index_copilot_threads_on_user_id"
+ t.index ["uuid"], name: "index_copilot_threads_on_uuid", unique: true
+ end
+
create_table "csat_survey_responses", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "conversation_id", null: false
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb
new file mode 100644
index 000000000..2a30fba48
--- /dev/null
+++ b/enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb
@@ -0,0 +1,25 @@
+class Api::V1::Accounts::Captain::CopilotMessagesController < Api::V1::Accounts::BaseController
+ before_action :current_account
+ before_action -> { check_authorization(Captain::Assistant) }
+ before_action :set_copilot_thread
+
+ def index
+ @copilot_messages = @copilot_thread
+ .copilot_messages
+ .order(created_at: :asc)
+ .page(permitted_params[:page] || 1)
+ .per(1000)
+ end
+
+ private
+
+ def set_copilot_thread
+ @copilot_thread = Current.account.copilot_threads.find_by!(
+ uuid: params[:copilot_thread_id], user_id: Current.user.id
+ )
+ end
+
+ def permitted_params
+ params.permit(:page)
+ end
+end
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb
new file mode 100644
index 000000000..e313f448c
--- /dev/null
+++ b/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb
@@ -0,0 +1,19 @@
+class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::BaseController
+ before_action :current_account
+ before_action -> { check_authorization(Captain::Assistant) }
+
+ def index
+ @copilot_threads = Current.account.copilot_threads
+ .where(user_id: Current.user.id)
+ .includes(:user)
+ .order(created_at: :desc)
+ .page(permitted_params[:page] || 1)
+ .per(5)
+ end
+
+ private
+
+ def permitted_params
+ params.permit(:page)
+ end
+end
diff --git a/enterprise/app/models/copilot_message.rb b/enterprise/app/models/copilot_message.rb
new file mode 100644
index 000000000..16ae2c3c9
--- /dev/null
+++ b/enterprise/app/models/copilot_message.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: copilot_messages
+#
+# id :bigint not null, primary key
+# message :jsonb not null
+# message_type :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint not null
+# copilot_thread_id :bigint not null
+# user_id :bigint not null
+#
+# Indexes
+#
+# index_copilot_messages_on_account_id (account_id)
+# index_copilot_messages_on_copilot_thread_id (copilot_thread_id)
+# index_copilot_messages_on_user_id (user_id)
+#
+class CopilotMessage < ApplicationRecord
+ belongs_to :copilot_thread
+ belongs_to :user
+ belongs_to :account
+
+ validates :message_type, presence: true, inclusion: { in: %w[user assistant assistant_thinking] }
+ validates :message, presence: true
+end
diff --git a/enterprise/app/models/copilot_thread.rb b/enterprise/app/models/copilot_thread.rb
new file mode 100644
index 000000000..865418ad7
--- /dev/null
+++ b/enterprise/app/models/copilot_thread.rb
@@ -0,0 +1,26 @@
+# == Schema Information
+#
+# Table name: copilot_threads
+#
+# id :bigint not null, primary key
+# title :string not null
+# uuid :uuid not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint not null
+# user_id :bigint not null
+#
+# Indexes
+#
+# index_copilot_threads_on_account_id (account_id)
+# index_copilot_threads_on_user_id (user_id)
+# index_copilot_threads_on_uuid (uuid) UNIQUE
+#
+class CopilotThread < ApplicationRecord
+ belongs_to :user
+ belongs_to :account
+ has_many :copilot_messages, dependent: :destroy
+
+ validates :title, presence: true
+ validates :uuid, presence: true, uniqueness: true
+end
diff --git a/enterprise/app/models/enterprise/concerns/account.rb b/enterprise/app/models/enterprise/concerns/account.rb
index 4fcb9b34b..4a573a4c4 100644
--- a/enterprise/app/models/enterprise/concerns/account.rb
+++ b/enterprise/app/models/enterprise/concerns/account.rb
@@ -9,5 +9,7 @@ module Enterprise::Concerns::Account
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
has_many :captain_documents, dependent: :destroy_async, class_name: 'Captain::Document'
+
+ has_many :copilot_threads, dependent: :destroy_async
end
end
diff --git a/enterprise/app/models/enterprise/concerns/user.rb b/enterprise/app/models/enterprise/concerns/user.rb
index 5d2687fbf..0e597b8d8 100644
--- a/enterprise/app/models/enterprise/concerns/user.rb
+++ b/enterprise/app/models/enterprise/concerns/user.rb
@@ -5,6 +5,8 @@ module Enterprise::Concerns::User
before_validation :ensure_installation_pricing_plan_quantity, on: :create
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
+ has_many :copilot_threads, dependent: :destroy_async
+ has_many :copilot_messages, dependent: :destroy_async
end
def ensure_installation_pricing_plan_quantity
diff --git a/enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder
new file mode 100644
index 000000000..ce0d5b175
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder
@@ -0,0 +1,8 @@
+json.payload do
+ json.array! @copilot_messages do |message|
+ json.id message.id
+ json.message message.message
+ json.message_type message.message_type
+ json.created_at message.created_at.to_i
+ end
+end
diff --git a/enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder
new file mode 100644
index 000000000..c06182ffd
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder
@@ -0,0 +1,12 @@
+json.payload do
+ json.array! @copilot_threads do |thread|
+ json.id thread.id
+ json.title thread.title
+ json.uuid thread.uuid
+ json.created_at thread.created_at.to_i
+ json.user do
+ json.id thread.user.id
+ json.name thread.user.name
+ end
+ end
+end
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb
new file mode 100644
index 000000000..0ccca90c5
--- /dev/null
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :request do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account: account, role: :administrator) }
+ let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user) }
+ let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, user: user, account: account) }
+
+ describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.uuid}/copilot_messages' do
+ context 'when it is an authenticated user' do
+ it 'returns all messages' do
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.uuid}/copilot_messages",
+ headers: user.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response['payload'].length).to eq(1)
+ expect(json_response['payload'][0]['id']).to eq(copilot_message.id)
+ end
+ end
+
+ context 'when thread uuid is invalid' do
+ it 'returns not found error' do
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads/invalid-uuid/copilot_messages",
+ headers: user.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb
new file mode 100644
index 000000000..8533a2d1d
--- /dev/null
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb
@@ -0,0 +1,50 @@
+require 'rails_helper'
+
+RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
+ let(:account) { create(:account) }
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ def json_response
+ JSON.parse(response.body, symbolize_names: true)
+ end
+
+ describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads' do
+ context 'when it is an un-authenticated user' do
+ it 'does not fetch copilot threads' do
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
+ as: :json
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an agent' do
+ it 'fetches copilot threads for the current user' do
+ # Create threads for the current agent
+ create_list(:captain_copilot_thread, 3, account: account, user: agent)
+ # Create threads for another user (should not be included)
+ create_list(:captain_copilot_thread, 2, account: account, user: admin)
+
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(json_response[:payload].length).to eq(3)
+
+ expect(json_response[:payload].map { |thread| thread[:user][:id] }.uniq).to eq([agent.id])
+ end
+
+ it 'returns threads in descending order of creation' do
+ threads = create_list(:captain_copilot_thread, 3, account: account, user: agent)
+
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(json_response[:payload].pluck(:id)).to eq(threads.reverse.pluck(:id))
+ end
+ end
+ end
+end
diff --git a/spec/factories/captain/copilot_message.rb b/spec/factories/captain/copilot_message.rb
new file mode 100644
index 000000000..78f9f202e
--- /dev/null
+++ b/spec/factories/captain/copilot_message.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :captain_copilot_message, class: 'CopilotMessage' do
+ account
+ user
+ copilot_thread { association :captain_copilot_thread }
+ message { { content: 'This is a test message' } }
+ message_type { 'user' }
+ end
+end
diff --git a/spec/factories/captain/copilot_thread.rb b/spec/factories/captain/copilot_thread.rb
new file mode 100644
index 000000000..fee78a7e7
--- /dev/null
+++ b/spec/factories/captain/copilot_thread.rb
@@ -0,0 +1,8 @@
+FactoryBot.define do
+ factory :captain_copilot_thread, class: 'CopilotThread' do
+ account
+ user
+ title { Faker::Lorem.sentence }
+ uuid { SecureRandom.uuid }
+ end
+end
From bce1f58e86a298a7694d2c35bfa4c9eb5f85d14b Mon Sep 17 00:00:00 2001
From: Pranav
Date: Thu, 15 May 2025 17:47:37 -0700
Subject: [PATCH 2/6] chore: Update LLM formatter classes to include additional
details (#11491)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR introduces support for optionally exposing more data during LLM
function calls. This will be useful as we expand Copilot’s capabilities.
Changes included:
- Add support for ArticleLlmFormatter
- Add missing specs for ContactLLMFormatter and ArticleLLMFormatter
- Add additional spec for ConversationLLMFormatter based on config
---
app/models/article.rb | 1 +
app/models/concerns/llm_formattable.rb | 4 +-
.../llm_formatter/article_llm_formatter.rb | 22 ++++++
.../llm_formatter/contact_llm_formatter.rb | 2 +-
.../conversation_llm_formatter.rb | 3 +-
.../llm_formatter/default_llm_formatter.rb | 2 +-
.../llm_text_formatter_service.rb | 4 +-
spec/models/article_spec.rb | 22 ++++++
.../article_llm_formatter_spec.rb | 44 +++++++++++
.../contact_llm_formatter_spec.rb | 78 +++++++++++++++++++
.../conversation_llm_formatter_spec.rb | 14 ++++
11 files changed, 189 insertions(+), 7 deletions(-)
create mode 100644 app/services/llm_formatter/article_llm_formatter.rb
create mode 100644 spec/services/llm_formatter/article_llm_formatter_spec.rb
create mode 100644 spec/services/llm_formatter/contact_llm_formatter_spec.rb
diff --git a/app/models/article.rb b/app/models/article.rb
index 48e0529a2..14450b574 100644
--- a/app/models/article.rb
+++ b/app/models/article.rb
@@ -33,6 +33,7 @@
#
class Article < ApplicationRecord
include PgSearch::Model
+ include LlmFormattable
has_many :associated_articles,
class_name: :Article,
diff --git a/app/models/concerns/llm_formattable.rb b/app/models/concerns/llm_formattable.rb
index 086ccc46a..0cdc76718 100644
--- a/app/models/concerns/llm_formattable.rb
+++ b/app/models/concerns/llm_formattable.rb
@@ -1,7 +1,7 @@
module LlmFormattable
extend ActiveSupport::Concern
- def to_llm_text
- LlmFormatter::LlmTextFormatterService.new(self).format
+ def to_llm_text(config = {})
+ LlmFormatter::LlmTextFormatterService.new(self).format(config)
end
end
diff --git a/app/services/llm_formatter/article_llm_formatter.rb b/app/services/llm_formatter/article_llm_formatter.rb
new file mode 100644
index 000000000..5df7976b7
--- /dev/null
+++ b/app/services/llm_formatter/article_llm_formatter.rb
@@ -0,0 +1,22 @@
+class LlmFormatter::ArticleLlmFormatter
+ attr_reader :article
+
+ def initialize(article)
+ @article = article
+ end
+
+ def format(*)
+ <<~TEXT
+ Title: #{article.title}
+ ID: #{article.id}
+ Status: #{article.status}
+ Category: #{article.category&.name || 'Uncategorized'}
+ Author: #{article.author&.name || 'Unknown'}
+ Views: #{article.views}
+ Created At: #{article.created_at}
+ Updated At: #{article.updated_at}
+ Content:
+ #{article.content}
+ TEXT
+ end
+end
diff --git a/app/services/llm_formatter/contact_llm_formatter.rb b/app/services/llm_formatter/contact_llm_formatter.rb
index 9dcfcd299..586f8743b 100644
--- a/app/services/llm_formatter/contact_llm_formatter.rb
+++ b/app/services/llm_formatter/contact_llm_formatter.rb
@@ -1,5 +1,5 @@
class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter
- def format
+ def format(*)
sections = []
sections << "Contact ID: ##{@record.id}"
sections << 'Contact Attributes:'
diff --git a/app/services/llm_formatter/conversation_llm_formatter.rb b/app/services/llm_formatter/conversation_llm_formatter.rb
index 07afc2cac..1444d75c1 100644
--- a/app/services/llm_formatter/conversation_llm_formatter.rb
+++ b/app/services/llm_formatter/conversation_llm_formatter.rb
@@ -1,5 +1,5 @@
class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter
- def format
+ def format(config = {})
sections = []
sections << "Conversation ID: ##{@record.display_id}"
sections << "Channel: #{@record.inbox.channel.name}"
@@ -10,6 +10,7 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter
'No messages in this conversation'
end
+ sections << "Contact Details: #{@record.contact.to_llm_text}" if config[:include_contact_details]
sections.join("\n")
end
diff --git a/app/services/llm_formatter/default_llm_formatter.rb b/app/services/llm_formatter/default_llm_formatter.rb
index b82a039cf..3ac1fa7f4 100644
--- a/app/services/llm_formatter/default_llm_formatter.rb
+++ b/app/services/llm_formatter/default_llm_formatter.rb
@@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter
@record = record
end
- def format
+ def format(*)
# override this
end
end
diff --git a/app/services/llm_formatter/llm_text_formatter_service.rb b/app/services/llm_formatter/llm_text_formatter_service.rb
index 0f1f37cf8..198f09d58 100644
--- a/app/services/llm_formatter/llm_text_formatter_service.rb
+++ b/app/services/llm_formatter/llm_text_formatter_service.rb
@@ -3,9 +3,9 @@ class LlmFormatter::LlmTextFormatterService
@record = record
end
- def format
+ def format(config = {})
formatter_class = find_formatter
- formatter_class.new(@record).format
+ formatter_class.new(@record).format(config)
end
private
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
index 0dc17d472..161f3541d 100644
--- a/spec/models/article_spec.rb
+++ b/spec/models/article_spec.rb
@@ -167,4 +167,26 @@ RSpec.describe Article do
end
end
end
+
+ describe '#to_llm_text' do
+ it 'returns formatted article text' do
+ category = create(:category, name: 'Test Category', slug: 'test_category', portal_id: portal_1.id)
+ article = create(:article, title: 'Test Article', category_id: category.id, content: 'This is the content', portal_id: portal_1.id,
+ author_id: user.id)
+ expected_output = <<~TEXT
+ Title: #{article.title}
+ ID: #{article.id}
+ Status: #{article.status}
+ Category: #{category.name}
+ Author: #{user.name}
+ Views: #{article.views}
+ Created At: #{article.created_at}
+ Updated At: #{article.updated_at}
+ Content:
+ #{article.content}
+ TEXT
+
+ expect(article.to_llm_text).to eq(expected_output)
+ end
+ end
end
diff --git a/spec/services/llm_formatter/article_llm_formatter_spec.rb b/spec/services/llm_formatter/article_llm_formatter_spec.rb
new file mode 100644
index 000000000..0f47beddb
--- /dev/null
+++ b/spec/services/llm_formatter/article_llm_formatter_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe LlmFormatter::ArticleLlmFormatter do
+ let(:account) { create(:account) }
+ let(:portal) { create(:portal, account: account) }
+ let(:category) { create(:category, slug: 'test_category', portal: portal, account: account) }
+ let(:author) { create(:user, account: account) }
+ let(:formatter) { described_class.new(article) }
+
+ describe '#format' do
+ context 'when article has all details' do
+ let(:article) do
+ create(:article,
+ slug: 'test_article',
+ portal: portal, category: category, author: author, views: 100, account: account)
+ end
+
+ it 'formats article details correctly' do
+ expected_output = <<~TEXT
+ Title: #{article.title}
+ ID: #{article.id}
+ Status: #{article.status}
+ Category: #{category.name}
+ Author: #{author.name}
+ Views: #{article.views}
+ Created At: #{article.created_at}
+ Updated At: #{article.updated_at}
+ Content:
+ #{article.content}
+ TEXT
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+
+ context 'when article has no category' do
+ let(:article) { create(:article, portal: portal, category: nil, author: author, account: account) }
+
+ it 'shows Uncategorized for category' do
+ expect(formatter.format).to include('Category: Uncategorized')
+ end
+ end
+ end
+end
diff --git a/spec/services/llm_formatter/contact_llm_formatter_spec.rb b/spec/services/llm_formatter/contact_llm_formatter_spec.rb
new file mode 100644
index 000000000..bf3345b98
--- /dev/null
+++ b/spec/services/llm_formatter/contact_llm_formatter_spec.rb
@@ -0,0 +1,78 @@
+require 'rails_helper'
+
+RSpec.describe LlmFormatter::ContactLlmFormatter do
+ let(:account) { create(:account) }
+ let(:contact) { create(:contact, account: account, name: 'John Doe', email: 'john@example.com', phone_number: '+1234567890') }
+ let(:formatter) { described_class.new(contact) }
+
+ describe '#format' do
+ context 'when contact has no notes' do
+ it 'formats contact details correctly' do
+ expected_output = [
+ "Contact ID: ##{contact.id}",
+ 'Contact Attributes:',
+ 'Name: John Doe',
+ 'Email: john@example.com',
+ 'Phone: +1234567890',
+ 'Location: ',
+ 'Country Code: ',
+ 'Contact Notes:',
+ 'No notes for this contact'
+ ].join("\n")
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+
+ context 'when contact has notes' do
+ before do
+ create(:note, account: account, contact: contact, content: 'First interaction')
+ create(:note, account: account, contact: contact, content: 'Follow up needed')
+ end
+
+ it 'includes notes in the output' do
+ expected_output = [
+ "Contact ID: ##{contact.id}",
+ 'Contact Attributes:',
+ 'Name: John Doe',
+ 'Email: john@example.com',
+ 'Phone: +1234567890',
+ 'Location: ',
+ 'Country Code: ',
+ 'Contact Notes:',
+ ' - First interaction',
+ ' - Follow up needed'
+ ].join("\n")
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+
+ context 'when contact has custom attributes' do
+ let!(:custom_attribute) do
+ create(:custom_attribute_definition, account: account, attribute_model: 'contact_attribute', attribute_display_name: 'Company')
+ end
+
+ before do
+ contact.update(custom_attributes: { custom_attribute.attribute_key => 'Acme Inc' })
+ end
+
+ it 'includes custom attributes in the output' do
+ expected_output = [
+ "Contact ID: ##{contact.id}",
+ 'Contact Attributes:',
+ 'Name: John Doe',
+ 'Email: john@example.com',
+ 'Phone: +1234567890',
+ 'Location: ',
+ 'Country Code: ',
+ 'Company: Acme Inc',
+ 'Contact Notes:',
+ 'No notes for this contact'
+ ].join("\n")
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+ end
+end
diff --git a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb
index 3838b8126..93fec14f7 100644
--- a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb
+++ b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb
@@ -47,5 +47,19 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do
expect(formatter.format).to eq(expected_output)
end
end
+
+ context 'when include_contact_details is true' do
+ it 'includes contact details' do
+ expected_output = [
+ "Conversation ID: ##{conversation.display_id}",
+ "Channel: #{conversation.inbox.channel.name}",
+ 'Message History:',
+ 'No messages in this conversation',
+ "Contact Details: #{conversation.contact.to_llm_text}"
+ ].join("\n")
+
+ expect(formatter.format(include_contact_details: true)).to eq(expected_output)
+ end
+ end
end
end
From e9cda40b71efa03e99d7679ab7ac5d670d10f59e Mon Sep 17 00:00:00 2001
From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
Date: Fri, 16 May 2025 10:26:37 +0700
Subject: [PATCH 3/6] fix: Twilio multiple attachment fix (#11452)
---
app/controllers/twilio/callback_controller.rb | 7 ++--
.../twilio/incoming_message_service.rb | 29 ++++++++-------
.../twilio/incoming_message_service_spec.rb | 35 +++++++++++++++++--
3 files changed, 54 insertions(+), 17 deletions(-)
diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb
index ff5db952d..ff16e9386 100644
--- a/app/controllers/twilio/callback_controller.rb
+++ b/app/controllers/twilio/callback_controller.rb
@@ -24,9 +24,10 @@ class Twilio::CallbackController < ApplicationController
:Body,
:ToCountry,
:FromState,
- :MediaUrl0,
- :MediaContentType0,
- :MessagingServiceSid
+ *Array.new(10) { |i| :"MediaUrl#{i}" },
+ *Array.new(10) { |i| :"MediaContentType#{i}" },
+ :MessagingServiceSid,
+ :NumMedia
)
end
end
diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb
index 5c5854ee6..7a335c87d 100644
--- a/app/services/twilio/incoming_message_service.rb
+++ b/app/services/twilio/incoming_message_service.rb
@@ -106,15 +106,22 @@ class Twilio::IncomingMessageService
end
def attach_files
- return if params[:MediaUrl0].blank?
+ num_media = params[:NumMedia].to_i
+ return if num_media.zero?
- attachment_file = download_attachment_file
+ num_media.times do |i|
+ media_url = params[:"MediaUrl#{i}"]
+ attach_single_file(media_url) if media_url.present?
+ end
+ end
+ def attach_single_file(media_url)
+ attachment_file = download_attachment_file(media_url)
return if attachment_file.blank?
@message.attachments.new(
account_id: @message.account_id,
- file_type: file_type(params[:MediaContentType0]),
+ file_type: file_type(attachment_file.content_type),
file: {
io: attachment_file,
filename: attachment_file.original_filename,
@@ -123,24 +130,22 @@ class Twilio::IncomingMessageService
)
end
- def download_attachment_file
- download_with_auth
+ def download_attachment_file(media_url)
+ download_with_auth(media_url)
rescue Down::Error, Down::ClientError => e
- handle_download_attachment_error(e)
+ handle_download_attachment_error(e, media_url)
end
- def download_with_auth
+ def download_with_auth(media_url)
Down.download(
- params[:MediaUrl0],
- # https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
+ media_url,
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
)
end
- # This is just a temporary workaround since some users have not yet enabled media protection. We will remove this in the future.
- def handle_download_attachment_error(error)
+ def handle_download_attachment_error(error, media_url)
Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying"
- Down.download(params[:MediaUrl0])
+ Down.download(media_url)
rescue StandardError => e
Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
nil
diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb
index 5f4dcec69..c8812e45d 100644
--- a/spec/services/twilio/incoming_message_service_spec.rb
+++ b/spec/services/twilio/incoming_message_service_spec.rb
@@ -173,7 +173,7 @@ describe Twilio::IncomingMessageService do
context 'when a message with an attachment is received' do
before do
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
- .to_return(status: 200, body: 'image data', headers: {})
+ .to_return(status: 200, body: 'image data', headers: { 'Content-Type' => 'image/png' })
end
let(:params_with_attachment) do
@@ -203,7 +203,7 @@ describe Twilio::IncomingMessageService do
.to_raise(Down::Error.new('Download error'))
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
- .to_return(status: 200, body: 'image data', headers: {})
+ .to_return(status: 200, body: 'image data', headers: { 'Content-Type' => 'image/png' })
end
let(:params_with_attachment_error) do
@@ -229,5 +229,36 @@ describe Twilio::IncomingMessageService do
expect(conversation.reload.messages.last.attachments.first.file_type).to eq('image')
end
end
+
+ context 'when a message with multiple attachments is received' do
+ before do
+ stub_request(:get, 'https://chatwoot-assets.local/sample.png')
+ .to_return(status: 200, body: 'image data 1', headers: { 'Content-Type' => 'image/png' })
+ stub_request(:get, 'https://chatwoot-assets.local/sample.jpg')
+ .to_return(status: 200, body: 'image data 2', headers: { 'Content-Type' => 'image/jpeg' })
+ end
+
+ let(:params_with_multiple_attachments) do
+ {
+ SmsSid: 'SMxx',
+ From: '+12345',
+ AccountSid: 'ACxxx',
+ MessagingServiceSid: twilio_channel.messaging_service_sid,
+ Body: 'testing multiple media',
+ NumMedia: '2',
+ MediaContentType0: 'image/png',
+ MediaUrl0: 'https://chatwoot-assets.local/sample.png',
+ MediaContentType1: 'image/jpeg',
+ MediaUrl1: 'https://chatwoot-assets.local/sample.jpg'
+ }
+ end
+
+ it 'creates a new message with multiple media attachments in existing conversation' do
+ described_class.new(params: params_with_multiple_attachments).perform
+ expect(conversation.reload.messages.last.content).to eq('testing multiple media')
+ expect(conversation.reload.messages.last.attachments.count).to eq(2)
+ expect(conversation.reload.messages.last.attachments.map(&:file_type)).to contain_exactly('image', 'image')
+ end
+ end
end
end
From d0611cb7f2dd39c8df98a7054d05848e4d54eb61 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Fri, 16 May 2025 14:18:52 +0530
Subject: [PATCH 4/6] feat: Improve CSAT responses (#11485)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Pull Request Template
## Description
This PR introduces basic customization options for the CSAT survey:
* **Display Type**: Option to use star ratings instead of emojis.
* **Message Text**: Customize the survey message (up to 200 characters).
* **Survey Rules**: Send surveys based on labels — trigger when a
conversation has or doesn't have a specific label.
Fixes
https://linear.app/chatwoot/document/improve-csat-responses-a61cf30e054e
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
### Loom videos
**Website Channel (Widget)**
https://www.loom.com/share/7f47836cde7940ae9d17b7997d060a18?sid=aad2ad0a-140a-4a09-8829-e01fa2e102c5
**Email Channel (Survey link)**
https://www.loom.com/share/e92f4c4c0f73417ba300a25885e093ce?sid=4bb006f0-1c2a-4352-a232-8bf684e3d757
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav
---
.../api/v1/accounts/inboxes_controller.rb | 18 +-
.../filter/inputs/FilterSelect.vue | 6 +-
.../components-next/message/bubbles/CSAT.vue | 33 ++-
.../dashboard/i18n/locale/en/inboxMgmt.json | 31 ++-
.../dashboard/settings/inbox/Settings.vue | 27 +-
.../settingsPage/CustomerSatisfactionPage.vue | 233 ++++++++++++++++++
.../components/CSATDisplayTypeSelector.vue | 26 ++
.../components/CSATEmojiInput.vue | 43 ++++
.../settingsPage/components/CSATStarInput.vue | 36 +++
.../components/CustomerSatisfaction.vue | 33 ++-
.../shared/components/StarRating.vue | 65 +++++
app/javascript/shared/constants/messages.js | 5 +
app/javascript/survey/App.vue | 2 +-
app/javascript/survey/assets/scss/woot.scss | 1 +
app/javascript/survey/components/Banner.vue | 2 +-
app/javascript/survey/components/Feedback.vue | 2 +-
app/javascript/survey/views/Response.vue | 44 +++-
.../widget/components/AgentMessageBubble.vue | 2 +
app/models/inbox.rb | 1 +
.../message_templates/template/csat_survey.rb | 58 ++++-
app/views/api/v1/models/_inbox.json.jbuilder | 1 +
.../api/v1/models/_csat_survey.json.jbuilder | 2 +
...250514045638_add_csat_config_to_inboxes.rb | 5 +
db/schema.rb | 3 +-
.../v1/accounts/inboxes_controller_spec.rb | 88 +++++++
.../template/csat_survey_spec.rb | 99 +++++++-
26 files changed, 812 insertions(+), 54 deletions(-)
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/CSATDisplayTypeSelector.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/CSATEmojiInput.vue
create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/CSATStarInput.vue
create mode 100644 app/javascript/shared/components/StarRating.vue
create mode 100644 db/migrate/20250514045638_add_csat_config_to_inboxes.rb
diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb
index 011faaf28..61d16b2ca 100644
--- a/app/controllers/api/v1/accounts/inboxes_controller.rb
+++ b/app/controllers/api/v1/accounts/inboxes_controller.rb
@@ -42,7 +42,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def update
- @inbox.update!(permitted_params.except(:channel))
+ inbox_params = permitted_params.except(:channel, :csat_config)
+ inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present?
+ @inbox.update!(inbox_params)
update_inbox_working_hours
update_channel if channel_update_required?
end
@@ -121,10 +123,22 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@inbox.channel.save!
end
+ def format_csat_config(config)
+ {
+ display_type: config['display_type'] || 'emoji',
+ message: config['message'] || '',
+ survey_rules: {
+ operator: config.dig('survey_rules', 'operator') || 'contains',
+ values: config.dig('survey_rules', 'values') || []
+ }
+ }
+ end
+
def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
- :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name]
+ :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
+ { csat_config: [:display_type, :message, { survey_rules: [:operator, { values: [] }] }] }]
end
def permitted_params(channel_attributes = [])
diff --git a/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue b/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue
index db420920b..56bce8f9d 100644
--- a/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue
+++ b/app/javascript/dashboard/components-next/filter/inputs/FilterSelect.vue
@@ -25,6 +25,10 @@ const props = defineProps({
type: String,
default: 'faded',
},
+ label: {
+ type: String,
+ default: null,
+ },
});
const selected = defineModel({
@@ -56,7 +60,7 @@ const updateSelected = newValue => {
:variant
:icon="iconToRender"
:trailing-icon="selectedOption.icon ? false : true"
- :label="hideLabel ? null : selectedOption.label"
+ :label="label || (hideLabel ? null : selectedOption.label)"
@click="toggle"
/>
diff --git a/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue b/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue
index 211840944..6f86bd868 100644
--- a/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue
+++ b/app/javascript/dashboard/components-next/message/bubbles/CSAT.vue
@@ -2,10 +2,10 @@
import { computed } from 'vue';
import BaseBubble from './Base.vue';
import { useI18n } from 'vue-i18n';
-import { CSAT_RATINGS } from 'shared/constants/messages';
+import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
import { useMessageContext } from '../provider.js';
-const { contentAttributes } = useMessageContext();
+const { contentAttributes, content } = useMessageContext();
const { t } = useI18n();
const response = computed(() => {
@@ -16,6 +16,14 @@ const isRatingSubmitted = computed(() => {
return !!response.value.rating;
});
+const displayType = computed(() => {
+ return contentAttributes.value?.displayType || CSAT_DISPLAY_TYPES.EMOJI;
+});
+
+const isStarRating = computed(() => {
+ return displayType.value === CSAT_DISPLAY_TYPES.STAR;
+});
+
const rating = computed(() => {
if (isRatingSubmitted.value) {
return CSAT_RATINGS.find(
@@ -25,16 +33,33 @@ const rating = computed(() => {
return null;
});
+
+const starRatingValue = computed(() => {
+ return response.value.rating || 0;
+});
- {{ t('CONVERSATION.CSAT_REPLY_MESSAGE') }}
+ {{ content || t('CONVERSATION.CSAT_REPLY_MESSAGE') }}
-
{{ t('CONVERSATION.RATING_TITLE') }}
- - {{ t(rating.translationKey) }}
+ -
+ {{ t(rating.translationKey) }}
+
+ -
+
+
+
+
-
{{ t('CONVERSATION.FEEDBACK_TITLE') }}
diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
index 5716e050c..cc0fe4b33 100644
--- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
@@ -481,7 +481,8 @@
"PRE_CHAT_FORM": "Pre Chat Form",
"BUSINESS_HOURS": "Business Hours",
"WIDGET_BUILDER": "Widget Builder",
- "BOT_CONFIGURATION": "Bot Configuration"
+ "BOT_CONFIGURATION": "Bot Configuration",
+ "CSAT": "CSAT"
},
"SETTINGS": "Settings",
"FEATURES": {
@@ -502,9 +503,7 @@
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
"AUTO_ASSIGNMENT": "Enable auto assignment",
- "ENABLE_CSAT": "Enable CSAT",
"SENDER_NAME_SECTION": "Enable Agent Name in Email",
- "ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
@@ -578,6 +577,32 @@
"LABEL": "Visitors should provide their name and email address before starting the chat"
}
},
+ "CSAT": {
+ "TITLE": "Enable CSAT",
+ "SUBTITLE": "Automatically trigger CSAT surveys at the end of conversations to understand how customers feel about their support experience. Track satisfaction trends and identify areas for improvement over time.",
+ "DISPLAY_TYPE": {
+ "LABEL": "Display type"
+ },
+ "MESSAGE": {
+ "LABEL": "Message",
+ "PLACEHOLDER": "Please enter a message to show users with the form"
+ },
+ "SURVEY_RULE": {
+ "LABEL": "Survey rule",
+ "DESCRIPTION_PREFIX": "Send the survey if the conversation",
+ "DESCRIPTION_SUFFIX": "any of the labels",
+ "OPERATOR": {
+ "CONTAINS": "contains",
+ "DOES_NOT_CONTAINS": "does not contain"
+ },
+ "SELECT_PLACEHOLDER": "select labels"
+ },
+ "NOTE": "Note: CSAT surveys are sent only once per conversation",
+ "API": {
+ "SUCCESS_MESSAGE": "CSAT settings updated successfully",
+ "ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
+ }
+ },
"BUSINESS_HOURS": {
"TITLE": "Set your availability",
"SUBTITLE": "Set your availability on your livechat widget",
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
index 73d9963bd..d9551c427 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
@@ -15,6 +15,7 @@ import PreChatFormSettings from './PreChatForm/Settings.vue';
import WeeklyAvailability from './components/WeeklyAvailability.vue';
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
+import CustomerSatisfactionPage from './settingsPage/CustomerSatisfactionPage.vue';
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
import WidgetBuilder from './WidgetBuilder.vue';
import BotConfiguration from './components/BotConfiguration.vue';
@@ -28,6 +29,7 @@ export default {
BotConfiguration,
CollaboratorsPage,
ConfigurationPage,
+ CustomerSatisfactionPage,
FacebookReauthorize,
GreetingsEditor,
PreChatFormSettings,
@@ -53,7 +55,6 @@ export default {
greetingEnabled: true,
greetingMessage: '',
emailCollectEnabled: false,
- csatSurveyEnabled: false,
senderNameType: 'friendly',
businessName: '',
locktoSingleConversation: false,
@@ -107,6 +108,10 @@ export default {
key: 'businesshours',
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
},
+ {
+ key: 'csat',
+ name: this.$t('INBOX_MGMT.TABS.CSAT'),
+ },
];
if (this.isAWebWidgetInbox) {
@@ -277,7 +282,6 @@ export default {
this.greetingEnabled = this.inbox.greeting_enabled || false;
this.greetingMessage = this.inbox.greeting_message || '';
this.emailCollectEnabled = this.inbox.enable_email_collect;
- this.csatSurveyEnabled = this.inbox.csat_survey_enabled;
this.senderNameType = this.inbox.sender_name_type;
this.businessName = this.inbox.business_name;
this.allowMessagesAfterResolved =
@@ -300,7 +304,6 @@ export default {
id: this.currentInboxId,
name: this.selectedInboxName,
enable_email_collect: this.emailCollectEnabled,
- csat_survey_enabled: this.csatSurveyEnabled,
allow_messages_after_resolved: this.allowMessagesAfterResolved,
greeting_enabled: this.greetingEnabled,
greeting_message: this.greetingMessage || '',
@@ -589,21 +592,6 @@ export default {
-
-