From b7f3f72b9c2e171f37af252c30a19d29a80a03f0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 27 Jun 2025 10:48:07 +0530 Subject: [PATCH 01/31] fix: Reply time calculation for re-opened conversations (#11787) This PR fixes the reply time calculation for reopened conversations. Previously, when a customer sent a message to reopen a resolved conversation, the reply time metric would be calculated incorrectly because the `waiting_since` timestamp was not properly set before the reply event was dispatched. This would create a case where you'd have reporting events like the following ``` [[33955732, "reply_time", 19.0], [33955847, "reply_time", 24.0], [33955666, "reply_time", 89.0], [33955530, "conversation_bot_handoff", 4.0], [33955567, "first_response", 42.0], [33955745, "reply_time", 21.0], [33955934, "reply_time", 49.0], [33955906, "reply_time", 121.0], [33987938, "conversation_resolved", 26285.0], [35571005, "reply_time", 985492.0]] ``` Note the `reply_time` after `conversation_resolved` The fix ensures that `waiting_since` is correctly updated when conversations are reopened, either through incoming messages or manual status changes, resulting in accurate reply time metrics that measure only the time from the customer's new message to the agent's response. ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? The changes have been tested with comprehensive specs that verify: 1. **Reply time calculation after conversation reopening** - Ensures correct timestamps are used when calculating reply times for reopened conversations 2. **Waiting since updates on status changes** - Verifies that `waiting_since` is properly set when conversation status changes from resolved to open 3. **Test the happy path** - Happy path is tested to ensure the `reply_time` and `first_response_time` is correctly calculated Test instructions: 1. Create a conversation with the last message from a customer and resolve it 2. Have an agent reopen it and reply to it 4. When an agent replies, verify that the agent reply_time event is not created for this message To fix any existing data, I've written a small script: https://gist.github.com/scmmishra/fdf458863f2d971978327bbfd5232d0c --------- Co-authored-by: Muhsin Keloth --- app/listeners/reporting_event_listener.rb | 4 + app/models/conversation.rb | 10 ++ .../reporting_event_listener_spec.rb | 110 +++++++++++++++++ spec/models/conversation_spec.rb | 113 ++++++++++++++++++ 4 files changed, 237 insertions(+) diff --git a/app/listeners/reporting_event_listener.rb b/app/listeners/reporting_event_listener.rb index b31d899bb..9f22fe8de 100644 --- a/app/listeners/reporting_event_listener.rb +++ b/app/listeners/reporting_event_listener.rb @@ -47,6 +47,10 @@ class ReportingEventListener < BaseListener message = extract_message_and_account(event)[0] conversation = message.conversation waiting_since = event.data[:waiting_since] + + return if waiting_since.blank? + + # When waiting_since is nil, set reply_time to 0 reply_time = message.created_at.to_i - waiting_since.to_i reporting_event = ReportingEvent.new( diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 922118b09..d6c5d0e4a 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -198,11 +198,21 @@ class Conversation < ApplicationRecord private def execute_after_update_commit_callbacks + handle_resolved_status_change notify_status_change create_activity notify_conversation_updation end + def handle_resolved_status_change + # When conversation is resolved, clear waiting_since using update_column to avoid callbacks + return unless saved_change_to_status? && status == 'resolved' + + # rubocop:disable Rails/SkipsModelValidations + update_column(:waiting_since, nil) + # rubocop:enable Rails/SkipsModelValidations + end + def ensure_snooze_until_reset self.snoozed_until = nil unless snoozed? end diff --git a/spec/listeners/reporting_event_listener_spec.rb b/spec/listeners/reporting_event_listener_spec.rb index 9a6b8d123..0edf79556 100644 --- a/spec/listeners/reporting_event_listener_spec.rb +++ b/spec/listeners/reporting_event_listener_spec.rb @@ -66,6 +66,34 @@ describe ReportingEventListener do end describe '#reply_created' do + let(:contact) { create(:contact, account: account) } + + def create_customer_message(conversation, created_at: Time.current) + create(:message, + message_type: 'incoming', + account: account, + inbox: inbox, + conversation: conversation, + sender: contact, + created_at: created_at) + end + + def create_agent_message(conversation, created_at: Time.current, sender: user) + create(:message, + message_type: 'outgoing', + account: account, + inbox: inbox, + conversation: conversation, + sender: sender, + created_at: created_at) + end + + def create_reply_event(agent_message, waiting_since, event_time = nil) + Events::Base.new('reply.created', event_time || agent_message.created_at, + waiting_since: waiting_since, + message: agent_message) + end + it 'creates reply created event' do event = Events::Base.new('reply.created', Time.zone.now, waiting_since: 2.hours.ago, message: message) listener.reply_created(event) @@ -74,6 +102,88 @@ describe ReportingEventListener do expect(events.length).to be 1 expect(events.first.value).to be_within(1).of(7200) end + + context 'when conversation is reopened' do + let(:resolved_conversation) do + create(:conversation, account: account, inbox: inbox, assignee: user, + status: 'resolved', contact: contact) + end + + context 'when customer sends message after resolution' do + it 'calculates reply time from the reopening message' do + customer_message_time = 3.hours.ago + create_customer_message(resolved_conversation, created_at: customer_message_time) + + resolved_conversation.reload + expect(resolved_conversation.status).to eq('open') + + agent_reply_time = 1.hour.ago + agent_message = create_agent_message(resolved_conversation, created_at: agent_reply_time) + + event = create_reply_event(agent_message, customer_message_time) + listener.reply_created(event) + + events = account.reporting_events.where(name: 'reply_time', conversation_id: resolved_conversation.id) + expect(events.length).to be 1 + expect(events.first.value).to be_within(60).of(7200) + end + end + + context 'when conversation has multiple reopenings' do + it 'tracks reply time correctly for each reopening' do + create_customer_message(resolved_conversation, created_at: 5.hours.ago) + first_agent_reply = create_agent_message(resolved_conversation, created_at: 4.hours.ago) + + event = create_reply_event(first_agent_reply, 5.hours.ago) + listener.reply_created(event) + + resolved_conversation.update!(status: 'resolved') + + create_customer_message(resolved_conversation, created_at: 2.hours.ago) + second_agent_reply = create_agent_message(resolved_conversation, created_at: 1.5.hours.ago) + + event = create_reply_event(second_agent_reply, 2.hours.ago) + listener.reply_created(event) + + events = account.reporting_events.where(name: 'reply_time', conversation_id: resolved_conversation.id) + .order(created_at: :asc) + expect(events.length).to be 2 + expect(events.first.value).to be_within(60).of(3600) + expect(events.second.value).to be_within(60).of(1800) + end + end + + context 'when conversation is manually reopened' do + it 'sets waiting_since when first customer message arrives after manual reopening' do + resolved_conversation.update!(status: 'open') + + customer_message_time = 1.hour.ago + create_customer_message(resolved_conversation, created_at: customer_message_time) + + agent_reply_time = 15.minutes.ago + agent_message = create_agent_message(resolved_conversation, created_at: agent_reply_time) + + event = create_reply_event(agent_message, customer_message_time) + listener.reply_created(event) + + events = account.reporting_events.where(name: 'reply_time', conversation_id: resolved_conversation.id) + expect(events.length).to be 1 + expect(events.first.value).to be_within(60).of(2700) + end + end + + context 'when waiting_since is nil' do + it 'does not creates reply time events' do + agent_message = create_agent_message(resolved_conversation) + + event = create_reply_event(agent_message, nil) + listener.reply_created(event) + + events = account.reporting_events.where(name: 'reply_time', conversation_id: resolved_conversation.id) + expect(events.length).to be 0 + end + end + end end describe '#first_reply_created' do diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index aef91603d..a29359528 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -836,4 +836,117 @@ RSpec.describe Conversation do expect(message_window_service).to have_received(:can_reply?) end end + + describe 'reply time calculation flows' do + include ActiveJob::TestHelper + + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:contact) { create(:contact, account: account) } + let(:agent) { create(:user, account: account, role: :agent) } + let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, assignee: agent, waiting_since: nil) } + let(:conversation_start_time) { 5.hours.ago } + + before do + create(:inbox_member, user: agent, inbox: inbox) + # rubocop:disable Rails/SkipsModelValidations + conversation.update_column(:waiting_since, nil) + conversation.update_column(:created_at, conversation_start_time) + # rubocop:enable Rails/SkipsModelValidations + conversation.messages.destroy_all + conversation.reporting_events.destroy_all + conversation.reload + end + + def create_customer_message(conversation, created_at: Time.current) + message = nil + perform_enqueued_jobs do + message = create(:message, + message_type: 'incoming', + account: conversation.account, + inbox: conversation.inbox, + conversation: conversation, + sender: conversation.contact, + created_at: created_at) + end + message + end + + def create_agent_message(conversation, created_at: Time.current) + message = nil + perform_enqueued_jobs do + message = create(:message, + message_type: 'outgoing', + account: conversation.account, + inbox: conversation.inbox, + conversation: conversation, + sender: conversation.assignee, + created_at: created_at) + end + message + end + + it 'correctly tracks waiting_since and creates first response time events' do + create_customer_message(conversation, created_at: conversation_start_time) + conversation.reload + expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time) + + # Agent replies - this should create first response event + agent_reply1_time = 4.hours.ago + create_agent_message(conversation, created_at: agent_reply1_time) + + first_response_events = account.reporting_events.where(name: 'first_response', conversation_id: conversation.id) + expect(first_response_events.count).to eq(1) + expect(first_response_events.first.value).to be_within(1.second).of(1.hour) + + # the first response should also clear the waiting_since + conversation.reload + expect(conversation.waiting_since).to be_nil + end + + it 'does not reset waiting_since if customer sends another message' do + create_customer_message(conversation, created_at: conversation_start_time) + conversation.reload + expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time) + + create_customer_message(conversation, created_at: 3.hours.ago) + conversation.reload + expect(conversation.waiting_since).to be_within(1.second).of(conversation_start_time) + end + + it 'records the correct reply_time for subsequent messages' do + create_customer_message(conversation, created_at: conversation_start_time) + create_agent_message(conversation, created_at: 4.hours.ago) + create_customer_message(conversation, created_at: 3.hours.ago) + + create_agent_message(conversation, created_at: 2.hours.ago) + reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id) + expect(reply_events.count).to eq(1) + expect(reply_events.first.value).to be_within(1.second).of(1.hour) + + conversation.reload + expect(conversation.waiting_since).to be_nil + end + + it 'records zero reply time if an agent sends a message after resolution' do + create_customer_message(conversation, created_at: conversation_start_time) + create_agent_message(conversation, created_at: 4.hours.ago) + create_customer_message(conversation, created_at: 3.hours.ago) + + conversation.toggle_status + expect(conversation.status).to eq('resolved') + + conversation.toggle_status + expect(conversation.status).to eq('open') + + conversation.reload + expect(conversation.waiting_since).to be_nil + + create_agent_message(conversation, created_at: 1.hour.ago) + # update_waiting_since will ensure that no events were created since the waiting_since was nil + # if the event is created it should log zero value, we have handled that in the reporting_event_listener + reply_events = account.reporting_events.where(name: 'reply_time', conversation_id: conversation.id) + expect(reply_events.count).to eq(0) + end + end end From eea1ab3002760077fde6494a516e7482bf00aa6d Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 27 Jun 2025 15:48:04 -0700 Subject: [PATCH 02/31] fix: Add composite index on messages for csat_metrics API performance (#11831) This PR adds a composite index (:account_id, :content_type, :created_at) on the table messages. This index is added as a temporary fix for performance issues in the CSAT responses controller where we query messages with account_id, content_type and created_at. The current implementation (account.message.input_csat.count) times out with millions of messages. TODO: Create a dedicated csat_survey table and add entries when surveys are sent, then query this table instead of the entire messages table for better performance. --- app/models/message.rb | 1 + .../20250627195529_add_index_to_messages.rb | 20 +++++++++++++++++++ db/schema.rb | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250627195529_add_index_to_messages.rb diff --git a/app/models/message.rb b/app/models/message.rb index e7c4e9b6c..d4036416e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -24,6 +24,7 @@ # # Indexes # +# idx_messages_account_content_created (account_id,content_type,created_at) # index_messages_on_account_created_type (account_id,created_at,message_type) # index_messages_on_account_id (account_id) # index_messages_on_account_id_and_inbox_id (account_id,inbox_id) diff --git a/db/migrate/20250627195529_add_index_to_messages.rb b/db/migrate/20250627195529_add_index_to_messages.rb new file mode 100644 index 000000000..ff58c31d4 --- /dev/null +++ b/db/migrate/20250627195529_add_index_to_messages.rb @@ -0,0 +1,20 @@ +class AddIndexToMessages < ActiveRecord::Migration[7.0] + def change + # This index is added as a temporary fix for performance issues in the CSAT + # responses controller where we query messages with account_id, content_type + # and created_at. The current implementation (account.message.input_csat.count) + # times out with millions of messages. + # + # TODO: Create a dedicated csat_survey table and add entries when surveys are + # sent, then query this table instead of the entire messages table for better + # performance. + return if index_exists?( + :messages, + [:account_id, :content_type, :created_at], + name: 'idx_messages_account_content_created' + ) + + add_index :messages, [:account_id, :content_type, :created_at], + name: 'idx_messages_account_content_created', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index fdd0cce82..af0c89086 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.1].define(version: 2025_06_20_120000) do +ActiveRecord::Schema[7.1].define(version: 2025_06_27_195529) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -825,6 +825,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_06_20_120000) do t.text "processed_message_content" t.jsonb "sentiment", default: {} t.index "((additional_attributes -> 'campaign_id'::text))", name: "index_messages_on_additional_attributes_campaign_id", using: :gin + t.index ["account_id", "content_type", "created_at"], name: "idx_messages_account_content_created" t.index ["account_id", "created_at", "message_type"], name: "index_messages_on_account_created_type" t.index ["account_id", "inbox_id"], name: "index_messages_on_account_id_and_inbox_id" t.index ["account_id"], name: "index_messages_on_account_id" From ee4a0d448622e9cf552e3cc6477723ff3157cc6a Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 27 Jun 2025 16:58:50 -0700 Subject: [PATCH 03/31] fix: disable_ddl_transaction! on add_index action (#11833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All migrations will automatically be wrapped in a transaction. There are queries that you can’t execute inside a transaction. Adding index concurrently is one of them, we have to disable the transaction. I missed this in the earlier PR. #11831 --- db/migrate/20250627195529_add_index_to_messages.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/migrate/20250627195529_add_index_to_messages.rb b/db/migrate/20250627195529_add_index_to_messages.rb index ff58c31d4..eb6b95cdd 100644 --- a/db/migrate/20250627195529_add_index_to_messages.rb +++ b/db/migrate/20250627195529_add_index_to_messages.rb @@ -1,4 +1,6 @@ class AddIndexToMessages < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + def change # This index is added as a temporary fix for performance issues in the CSAT # responses controller where we query messages with account_id, content_type From b1893c7d96c41485b7b6af45609983e9d131c6a8 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 30 Jun 2025 11:35:32 +0530 Subject: [PATCH 04/31] fix: Support location messages in Twilio WhatsApp integration (#11830) Fixes location messages not appearing in conversations when sent via Twilio. Location messages were being filtered out due to empty body content and missing parameter handling. ![CleanShot 2025-06-27 at 20 48 12](https://github.com/user-attachments/assets/b5a75796-6937-49bc-b689-7d04f4ea5d09) --- app/controllers/twilio/callback_controller.rb | 5 ++++- app/jobs/webhooks/twilio_events_job.rb | 10 +++++++-- .../twilio/incoming_message_service.rb | 14 ++++++++++++ spec/jobs/webhooks/twilio_events_job_spec.rb | 21 ++++++++++++++++++ .../twilio/incoming_message_service_spec.rb | 22 +++++++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb index ff16e9386..455828228 100644 --- a/app/controllers/twilio/callback_controller.rb +++ b/app/controllers/twilio/callback_controller.rb @@ -27,7 +27,10 @@ class Twilio::CallbackController < ApplicationController *Array.new(10) { |i| :"MediaUrl#{i}" }, *Array.new(10) { |i| :"MediaContentType#{i}" }, :MessagingServiceSid, - :NumMedia + :NumMedia, + :Latitude, + :Longitude, + :MessageType ) end end diff --git a/app/jobs/webhooks/twilio_events_job.rb b/app/jobs/webhooks/twilio_events_job.rb index 5f44d981b..87fdfadfb 100644 --- a/app/jobs/webhooks/twilio_events_job.rb +++ b/app/jobs/webhooks/twilio_events_job.rb @@ -2,10 +2,16 @@ class Webhooks::TwilioEventsJob < ApplicationJob queue_as :low def perform(params = {}) - # Skip processing if Body parameter or MediaUrl0 is not present + # Skip processing if Body parameter, MediaUrl0, or location data is not present # This is to skip processing delivery events being delivered to this endpoint - return if params[:Body].blank? && params[:MediaUrl0].blank? + return if params[:Body].blank? && params[:MediaUrl0].blank? && !valid_location_message?(params) ::Twilio::IncomingMessageService.new(params: params).perform end + + private + + def valid_location_message?(params) + params[:MessageType] == 'location' && params[:Latitude].present? && params[:Longitude].present? + end end diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index c38577599..a11b99744 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -17,6 +17,7 @@ class Twilio::IncomingMessageService source_id: params[:SmsSid] ) attach_files + attach_location if location_message? @message.save! end @@ -155,4 +156,17 @@ class Twilio::IncomingMessageService Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping" nil end + + def location_message? + params[:MessageType] == 'location' && params[:Latitude].present? && params[:Longitude].present? + end + + def attach_location + @message.attachments.new( + account_id: @message.account_id, + file_type: :location, + coordinates_lat: params[:Latitude].to_f, + coordinates_long: params[:Longitude].to_f + ) + end end diff --git a/spec/jobs/webhooks/twilio_events_job_spec.rb b/spec/jobs/webhooks/twilio_events_job_spec.rb index f42caf675..aad70c68b 100644 --- a/spec/jobs/webhooks/twilio_events_job_spec.rb +++ b/spec/jobs/webhooks/twilio_events_job_spec.rb @@ -79,4 +79,25 @@ RSpec.describe Webhooks::TwilioEventsJob do described_class.perform_now(params_with_media) end end + + context 'when location message is present' do + let(:params_with_location) do + { + From: 'whatsapp:+1234567890', + To: 'whatsapp:+0987654321', + MessageType: 'location', + Latitude: '12.160894393921', + Longitude: '75.265205383301', + AccountSid: 'AC123', + SmsSid: 'SM123' + } + end + + it 'processes the location message' do + service = double + expect(Twilio::IncomingMessageService).to receive(:new).with(params: params_with_location).and_return(service) + expect(service).to receive(:perform) + described_class.perform_now(params_with_location) + end + end end diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb index c8812e45d..94fc3fdbf 100644 --- a/spec/services/twilio/incoming_message_service_spec.rb +++ b/spec/services/twilio/incoming_message_service_spec.rb @@ -260,5 +260,27 @@ describe Twilio::IncomingMessageService do expect(conversation.reload.messages.last.attachments.map(&:file_type)).to contain_exactly('image', 'image') end end + + context 'when a location message is received' do + let(:params_with_location) do + { + SmsSid: 'SMxx', + From: '+12345', + AccountSid: 'ACxxx', + MessagingServiceSid: twilio_channel.messaging_service_sid, + MessageType: 'location', + Latitude: '12.160894393921', + Longitude: '75.265205383301' + } + end + + it 'creates a message with location attachment' do + described_class.new(params: params_with_location).perform + + message = conversation.reload.messages.last + expect(message.attachments.count).to eq(1) + expect(message.attachments.first.file_type).to eq('location') + end + end end end From d7c10b4f2af568857d25e730a267c0ca352abb66 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:30:09 +0530 Subject: [PATCH 05/31] chore: Add "Coming Soon" overlay to voice channel selector (#11835) # Pull Request Template ### Screenshots **Dark** image **Light** image --------- Co-authored-by: Muhsin Keloth --- .eslintrc.js | 1 + .../dashboard/components/ChannelSelector.vue | 21 ++++++++++++++----- .../components/widgets/ChannelItem.vue | 7 +++++++ .../dashboard/i18n/locale/en/components.json | 3 +++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6c867f557..6b5205ad7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -103,6 +103,7 @@ module.exports = { '⌘', '📄', '🎉', + '🚀', '💬', '👥', '📥', diff --git a/app/javascript/dashboard/components/ChannelSelector.vue b/app/javascript/dashboard/components/ChannelSelector.vue index 1e09f5363..236ae3ded 100644 --- a/app/javascript/dashboard/components/ChannelSelector.vue +++ b/app/javascript/dashboard/components/ChannelSelector.vue @@ -9,20 +9,31 @@ export default { type: String, required: true, }, + isComingSoon: { + type: Boolean, + default: false, + }, }, }; @@ -33,7 +44,7 @@ export default { } &:hover { - @apply border-transparent shadow-none cursor-not-allowed; + @apply border-n-strong shadow-none cursor-not-allowed; } } diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 933e117a9..5bc96d2b5 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -57,6 +57,12 @@ export default { 'voice', ].includes(key); }, + isComingSoon() { + const { key } = this.channel; + // Show "Coming Soon" only if the channel is marked as coming soon + // and the corresponding feature flag is not enabled yet. + return ['voice'].includes(key) && !this.isActive; + }, }, methods: { getChannelThumbnail() { @@ -79,6 +85,7 @@ export default { :class="{ inactive: !isActive }" :title="channel.name" :src="getChannelThumbnail()" + :is-coming-soon="isComingSoon" @click="onItemClick" /> diff --git a/app/javascript/dashboard/i18n/locale/en/components.json b/app/javascript/dashboard/i18n/locale/en/components.json index e44c6e039..0a2542a84 100644 --- a/app/javascript/dashboard/i18n/locale/en/components.json +++ b/app/javascript/dashboard/i18n/locale/en/components.json @@ -49,5 +49,8 @@ "HOURS": "Hours", "DAYS": "Days", "PLACEHOLDER": "Enter duration" + }, + "CHANNEL_SELECTOR": { + "COMING_SOON": "Coming Soon!" } } From 6e207acb5aee4971b320737b12bea5ee7914b046 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:54:19 +0530 Subject: [PATCH 06/31] fix: CSAT table header and date range translation issue on reload (#11836) # Pull Request Template ## Description This PR fixes the translation issue in the CSAT reports table header and date range filter, where labels reverted to English after a page reload. Fixes https://linear.app/chatwoot/issue/CW-4557/language-switching-on-page-reload ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? https://github.com/user-attachments/assets/c68da978-1f17-44b5-bb21-5ea2668563fb ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] 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 - [ ] 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 --- .../settings/reports/components/CsatTable.vue | 8 ++-- .../reports/components/Filters/DateRange.vue | 45 ++++++++++--------- .../specs/Filters/FiltersDateRange.spec.js | 23 +++++----- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue index 8beeafd70..76a3e3079 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue @@ -61,7 +61,7 @@ const defaultSpanRender = cellProps => { const columnHelper = createColumnHelper(); -const columns = [ +const columns = computed(() => [ columnHelper.accessor('contact', { header: t('CSAT_REPORTS.TABLE.HEADER.CONTACT_NAME'), width: 200, @@ -121,7 +121,7 @@ const columns = [ width: 100, cell: cellProps => h(ConversationCell, cellProps), }), -]; +]); const paginationParams = computed(() => { return { @@ -134,7 +134,9 @@ const table = useVueTable({ get data() { return tableData.value; }, - columns, + get columns() { + return columns.value; + }, manualPagination: true, enableSorting: false, getCoreRowModel: getCoreRowModel(), diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/DateRange.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/DateRange.vue index 4d1cc340f..9caab26dd 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/DateRange.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/DateRange.vue @@ -1,28 +1,33 @@ - @@ -31,7 +36,7 @@ export default { { - it('emits "on-range-change" event when updateRange is called', () => { + it('emits "onRangeChange" event when updateRange is called', () => { const wrapper = shallowMount(ReportFiltersDateRange, mountParams); const selectedRange = DATE_RANGE_OPTIONS.LAST_7_DAYS; wrapper.vm.updateRange(selectedRange); - expect(wrapper.emitted('on-range-change')).toBeTruthy(); - expect(wrapper.emitted('on-range-change')[0]).toEqual([selectedRange]); + expect(wrapper.emitted('onRangeChange')).toBeTruthy(); + expect(wrapper.emitted('onRangeChange')[0]).toEqual([selectedRange]); }); it('initializes options correctly', () => { const wrapper = shallowMount(ReportFiltersDateRange, mountParams); - const expectedOptions = Object.values(DATE_RANGE_OPTIONS).map(option => ({ - ...option, - name: option.translationKey, - })); + const expectedIds = Object.values(DATE_RANGE_OPTIONS).map( + option => option.id + ); + const receivedIds = wrapper.vm.options.map(option => option.id); - expect(wrapper.vm.options).toEqual(expectedOptions); + expect(receivedIds).toEqual(expectedIds); }); it('initializes selectedOption correctly', () => { const wrapper = shallowMount(ReportFiltersDateRange, mountParams); - const expectedSelectedOption = Object.values(DATE_RANGE_OPTIONS)[0]; - expect(wrapper.vm.selectedOption).toEqual({ - ...expectedSelectedOption, - name: expectedSelectedOption.translationKey, - }); + const expectedId = Object.values(DATE_RANGE_OPTIONS)[0].id; + expect(wrapper.vm.selectedOption.id).toBe(expectedId); }); }); From a657b45bd1bb9e8ced580ad4a0de4adc77f9510e Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 1 Jul 2025 00:12:07 +0530 Subject: [PATCH 07/31] feat(revert): "feat: captain image support" (#11841) Reverts chatwoot/chatwoot#11730 --- .../accounts/captain/assistants_controller.rb | 4 +- .../conversation/response_builder_job.rb | 37 ++- .../captain/llm/assistant_chat_service.rb | 13 +- .../open_ai_message_builder_service.rb | 59 ---- .../captain/assistants_controller_spec.rb | 8 +- .../conversation/response_builder_job_spec.rb | 25 -- .../open_ai_message_builder_service_spec.rb | 309 ------------------ 7 files changed, 37 insertions(+), 418 deletions(-) delete mode 100644 enterprise/app/services/captain/open_ai_message_builder_service.rb delete mode 100644 spec/enterprise/services/captain/open_ai_message_builder_service_spec.rb diff --git a/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb index ec8e8e653..e5a055836 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb @@ -25,8 +25,8 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base def playground response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response( - additional_message: params[:message_content], - message_history: message_history + params[:message_content], + message_history ) render json: response diff --git a/enterprise/app/jobs/captain/conversation/response_builder_job.rb b/enterprise/app/jobs/captain/conversation/response_builder_job.rb index 431945896..f341a6e98 100644 --- a/enterprise/app/jobs/captain/conversation/response_builder_job.rb +++ b/enterprise/app/jobs/captain/conversation/response_builder_job.rb @@ -26,7 +26,8 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob def generate_and_process_response @response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response( - message_history: collect_previous_messages + @conversation.messages.incoming.last.content, + collect_previous_messages ) return process_action('handoff') if handoff_requested? @@ -42,11 +43,33 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob .where(message_type: [:incoming, :outgoing]) .where(private: false) .map do |message| - { - content: prepare_multimodal_message_content(message), - role: determine_role(message) - } + { + content: message_content(message), + role: determine_role(message) + } + end + end + + def message_content(message) + return message.content if message.content.present? + return 'User has shared a message without content' unless message.attachments.any? + + audio_transcriptions = extract_audio_transcriptions(message.attachments) + return audio_transcriptions if audio_transcriptions.present? + + 'User has shared an attachment' + end + + def extract_audio_transcriptions(attachments) + audio_attachments = attachments.where(file_type: :audio) + return '' if audio_attachments.blank? + + transcriptions = '' + audio_attachments.each do |attachment| + result = Messages::AudioTranscriptionService.new(attachment).perform + transcriptions += result[:transcriptions] if result[:success] end + transcriptions end def determine_role(message) @@ -55,10 +78,6 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob message.message_type == 'incoming' ? 'user' : 'system' end - def prepare_multimodal_message_content(message) - Captain::OpenAiMessageBuilderService.new(message: message).generate_content - end - def handoff_requested? @response['response'] == 'conversation_handoff' end diff --git a/enterprise/app/services/captain/llm/assistant_chat_service.rb b/enterprise/app/services/captain/llm/assistant_chat_service.rb index ca8fafaa0..569931d44 100644 --- a/enterprise/app/services/captain/llm/assistant_chat_service.rb +++ b/enterprise/app/services/captain/llm/assistant_chat_service.rb @@ -12,16 +12,9 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService register_tools end - # additional_message: A single message (String) from the user that should be appended to the chat. - # It can be an empty String or nil when you only want to supply historical messages. - # message_history: An Array of already formatted messages that provide the previous context. - # role: The role for the additional_message (defaults to `user`). - # - # NOTE: Parameters are provided as keyword arguments to improve clarity and avoid relying on - # positional ordering. - def generate_response(additional_message: nil, message_history: [], role: 'user') - @messages += message_history - @messages << { role: role, content: additional_message } if additional_message.present? + def generate_response(input, previous_messages = [], role = 'user') + @messages += previous_messages + @messages << { role: role, content: input } if input.present? request_chat_completion end diff --git a/enterprise/app/services/captain/open_ai_message_builder_service.rb b/enterprise/app/services/captain/open_ai_message_builder_service.rb deleted file mode 100644 index 3320ad537..000000000 --- a/enterprise/app/services/captain/open_ai_message_builder_service.rb +++ /dev/null @@ -1,59 +0,0 @@ -class Captain::OpenAiMessageBuilderService - pattr_initialize [:message!] - - def generate_content - parts = [] - parts << text_part(@message.content) if @message.content.present? - parts.concat(attachment_parts(@message.attachments)) if @message.attachments.any? - - return 'Message without content' if parts.blank? - return parts.first[:text] if parts.one? && parts.first[:type] == 'text' - - parts - end - - private - - def text_part(text) - { type: 'text', text: text } - end - - def image_part(image_url) - { type: 'image_url', image_url: { url: image_url } } - end - - def attachment_parts(attachments) - image_attachments = attachments.where(file_type: :image) - image_content = image_parts(image_attachments) - - transcription = extract_audio_transcriptions(attachments) - transcription_part = text_part(transcription) if transcription.present? - - attachment_part = text_part('User has shared an attachment') if attachments.where.not(file_type: %i[image audio]).exists? - - [image_content, transcription_part, attachment_part].flatten.compact - end - - def image_parts(image_attachments) - image_attachments.each_with_object([]) do |attachment, parts| - url = get_attachment_url(attachment) - parts << image_part(url) if url.present? - end - end - - def get_attachment_url(attachment) - return attachment.external_url if attachment.external_url.present? - - attachment.file.attached? ? attachment.file_url : nil - end - - def extract_audio_transcriptions(attachments) - audio_attachments = attachments.where(file_type: :audio) - return '' if audio_attachments.blank? - - audio_attachments.map do |attachment| - result = Messages::AudioTranscriptionService.new(attachment).perform - result[:success] ? result[:transcriptions] : '' - end.join - end -end \ No newline at end of file diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb index 80be6f30f..1f6d83d80 100644 --- a/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/captain/assistants_controller_spec.rb @@ -211,8 +211,8 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do expect(response).to have_http_status(:success) expect(chat_service).to have_received(:generate_response).with( - additional_message: valid_params[:message_content], - message_history: valid_params[:message_history] + valid_params[:message_content], + valid_params[:message_history] ) expect(json_response[:content]).to eq('Assistant response') end @@ -232,8 +232,8 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do expect(response).to have_http_status(:success) expect(chat_service).to have_received(:generate_response).with( - additional_message: params_without_history[:message_content], - message_history: [] + params_without_history[:message_content], + [] ) end end diff --git a/spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb b/spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb index ca8d4a6c0..1e4a6e824 100644 --- a/spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb +++ b/spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb @@ -30,30 +30,5 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do account.reload expect(account.usage_limits[:captain][:responses][:consumed]).to eq(1) end - - context 'when message contains an image' do - let(:message_with_image) { create(:message, conversation: conversation, message_type: :incoming, content: 'Can you help with this error?') } - let(:image_attachment) { message_with_image.attachments.create!(account: account, file_type: :image, external_url: 'https://example.com/error.jpg') } - - before do - image_attachment - end - - it 'includes image URL directly in the message content for OpenAI vision analysis' do - # Expect the generate_response to receive multimodal content with image URL - expect(mock_llm_chat_service).to receive(:generate_response) do |**kwargs| - history = kwargs[:message_history] - last_entry = history.last - expect(last_entry[:content]).to be_an(Array) - expect(last_entry[:content].any? { |part| part[:type] == 'text' && part[:text] == 'Can you help with this error?' }).to be true - expect(last_entry[:content].any? do |part| - part[:type] == 'image_url' && part[:image_url][:url] == 'https://example.com/error.jpg' - end).to be true - { 'response' => 'I can see the error in your image. It appears to be a database connection issue.' } - end - - described_class.perform_now(conversation, assistant) - end - end end end diff --git a/spec/enterprise/services/captain/open_ai_message_builder_service_spec.rb b/spec/enterprise/services/captain/open_ai_message_builder_service_spec.rb deleted file mode 100644 index 13c29f756..000000000 --- a/spec/enterprise/services/captain/open_ai_message_builder_service_spec.rb +++ /dev/null @@ -1,309 +0,0 @@ -require 'rails_helper' - -RSpec.describe Captain::OpenAiMessageBuilderService do - subject(:service) { described_class.new(message: message) } - - let(:message) { create(:message, content: 'Hello world') } - - describe '#generate_content' do - context 'when message has only text content' do - it 'returns the text content directly' do - expect(service.generate_content).to eq('Hello world') - end - end - - context 'when message has no content and no attachments' do - let(:message) { create(:message, content: nil) } - - it 'returns default message' do - expect(service.generate_content).to eq('Message without content') - end - end - - context 'when message has text content and attachments' do - before do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: 'https://example.com/image.jpg') - attachment.save! - end - - it 'returns an array of content parts' do - result = service.generate_content - expect(result).to be_an(Array) - expect(result).to include({ type: 'text', text: 'Hello world' }) - expect(result).to include({ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }) - end - end - - context 'when message has only non-text attachments' do - let(:message) { create(:message, content: nil) } - - before do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: 'https://example.com/image.jpg') - attachment.save! - end - - it 'returns an array of content parts without text' do - result = service.generate_content - expect(result).to be_an(Array) - expect(result).to include({ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }) - expect(result).not_to include(hash_including(type: 'text', text: 'Hello world')) - end - end - end - - describe '#attachment_parts' do - let(:message) { create(:message, content: nil) } - let(:attachments) { message.attachments } - - context 'with image attachments' do - before do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: 'https://example.com/image.jpg') - attachment.save! - end - - it 'includes image parts' do - result = service.send(:attachment_parts, attachments) - expect(result).to include({ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }) - end - end - - context 'with audio attachments' do - let(:audio_attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :audio) - attachment.save! - attachment - end - - before do - allow(Messages::AudioTranscriptionService).to receive(:new).with(audio_attachment).and_return( - instance_double(Messages::AudioTranscriptionService, perform: { success: true, transcriptions: 'Audio transcription text' }) - ) - end - - it 'includes transcription text part' do - audio_attachment # trigger creation - result = service.send(:attachment_parts, attachments) - expect(result).to include({ type: 'text', text: 'Audio transcription text' }) - end - end - - context 'with other file types' do - before do - attachment = message.attachments.build(account_id: message.account_id, file_type: :file) - attachment.save! - end - - it 'includes generic attachment message' do - result = service.send(:attachment_parts, attachments) - expect(result).to include({ type: 'text', text: 'User has shared an attachment' }) - end - end - - context 'with mixed attachment types' do - let(:image_attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: 'https://example.com/image.jpg') - attachment.save! - attachment - end - - let(:audio_attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :audio) - attachment.save! - attachment - end - - let(:document_attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :file) - attachment.save! - attachment - end - - before do - allow(Messages::AudioTranscriptionService).to receive(:new).with(audio_attachment).and_return( - instance_double(Messages::AudioTranscriptionService, perform: { success: true, transcriptions: 'Audio text' }) - ) - end - - it 'includes all relevant parts' do - image_attachment # trigger creation - audio_attachment # trigger creation - document_attachment # trigger creation - - result = service.send(:attachment_parts, attachments) - expect(result).to include({ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }) - expect(result).to include({ type: 'text', text: 'Audio text' }) - expect(result).to include({ type: 'text', text: 'User has shared an attachment' }) - end - end - end - - describe '#image_parts' do - let(:message) { create(:message, content: nil) } - - context 'with valid image attachments' do - let(:image1) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: 'https://example.com/image1.jpg') - attachment.save! - attachment - end - - let(:image2) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: 'https://example.com/image2.jpg') - attachment.save! - attachment - end - - it 'returns image parts for all valid images' do - image1 # trigger creation - image2 # trigger creation - - image_attachments = message.attachments.where(file_type: :image) - result = service.send(:image_parts, image_attachments) - - expect(result).to include({ type: 'image_url', image_url: { url: 'https://example.com/image1.jpg' } }) - expect(result).to include({ type: 'image_url', image_url: { url: 'https://example.com/image2.jpg' } }) - end - end - - context 'with image attachments without URLs' do - let(:image_attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image, external_url: nil) - attachment.save! - attachment - end - - before do - allow(image_attachment).to receive(:file).and_return(instance_double(ActiveStorage::Attached::One, attached?: false)) - end - - it 'skips images without valid URLs' do - image_attachment # trigger creation - - image_attachments = message.attachments.where(file_type: :image) - result = service.send(:image_parts, image_attachments) - - expect(result).to be_empty - end - end - end - - describe '#get_attachment_url' do - let(:attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :image) - attachment.save! - attachment - end - - context 'when attachment has external_url' do - before { attachment.update(external_url: 'https://example.com/image.jpg') } - - it 'returns external_url' do - expect(service.send(:get_attachment_url, attachment)).to eq('https://example.com/image.jpg') - end - end - - context 'when attachment has attached file' do - before do - attachment.update(external_url: nil) - allow(attachment).to receive(:file).and_return(instance_double(ActiveStorage::Attached::One, attached?: true)) - allow(attachment).to receive(:file_url).and_return('https://local.com/file.jpg') - end - - it 'returns file_url' do - expect(service.send(:get_attachment_url, attachment)).to eq('https://local.com/file.jpg') - end - end - - context 'when attachment has no URL or file' do - before do - attachment.update(external_url: nil) - allow(attachment).to receive(:file).and_return(instance_double(ActiveStorage::Attached::One, attached?: false)) - end - - it 'returns nil' do - expect(service.send(:get_attachment_url, attachment)).to be_nil - end - end - end - - describe '#extract_audio_transcriptions' do - let(:message) { create(:message, content: nil) } - - context 'with no audio attachments' do - it 'returns empty string' do - result = service.send(:extract_audio_transcriptions, message.attachments) - expect(result).to eq('') - end - end - - context 'with successful audio transcriptions' do - let(:audio1) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :audio) - attachment.save! - attachment - end - - let(:audio2) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :audio) - attachment.save! - attachment - end - - before do - allow(Messages::AudioTranscriptionService).to receive(:new).with(audio1).and_return( - instance_double(Messages::AudioTranscriptionService, perform: { success: true, transcriptions: 'First audio text. ' }) - ) - allow(Messages::AudioTranscriptionService).to receive(:new).with(audio2).and_return( - instance_double(Messages::AudioTranscriptionService, perform: { success: true, transcriptions: 'Second audio text.' }) - ) - end - - it 'concatenates all successful transcriptions' do - audio1 # trigger creation - audio2 # trigger creation - - attachments = message.attachments - result = service.send(:extract_audio_transcriptions, attachments) - expect(result).to eq('First audio text. Second audio text.') - end - end - - context 'with failed audio transcriptions' do - let(:audio_attachment) do - attachment = message.attachments.build(account_id: message.account_id, file_type: :audio) - attachment.save! - attachment - end - - before do - allow(Messages::AudioTranscriptionService).to receive(:new).with(audio_attachment).and_return( - instance_double(Messages::AudioTranscriptionService, perform: { success: false, transcriptions: nil }) - ) - end - - it 'returns empty string for failed transcriptions' do - audio_attachment # trigger creation - - attachments = message.attachments - result = service.send(:extract_audio_transcriptions, attachments) - expect(result).to eq('') - end - end - end - - describe 'private helper methods' do - describe '#text_part' do - it 'returns correct text part format' do - result = service.send(:text_part, 'Hello world') - expect(result).to eq({ type: 'text', text: 'Hello world' }) - end - end - - describe '#image_part' do - it 'returns correct image part format' do - result = service.send(:image_part, 'https://example.com/image.jpg') - expect(result).to eq({ type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }) - end - end - end -end From 58da92a252bd03437b42b1c2d281bb4cb0b2164e Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 30 Jun 2025 19:06:25 -0700 Subject: [PATCH 08/31] chore: Disable copilot usage after the response count is over (#11845) Disable copilot if the response usage is over. --- config/locales/en.yml | 1 + .../captain/copilot_threads_controller.rb | 15 ++++++++-- .../copilot_threads_controller_spec.rb | 30 ++++++++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index a41a009cf..221f83472 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -270,6 +270,7 @@ en: short_description: 'Sync your contacts and conversations with LeadSquared CRM.' description: 'Sync your contacts and conversations with LeadSquared CRM. This integration automatically creates leads in LeadSquared when new contacts are added, and logs conversation activity to provide your sales team with complete context.' captain: + copilot_message_required: Message is required copilot_error: 'Please connect an assistant to this inbox to use Copilot' copilot_limit: 'You are out of Copilot credits. You can buy more credits from the billing section.' copilot: 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 index 533859b95..8ea85145e 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb @@ -23,14 +23,25 @@ class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts:: message: { content: copilot_thread_params[:message] } ) - copilot_message.enqueue_response_job(copilot_thread_params[:conversation_id], Current.user.id) + build_copilot_response(copilot_message) end end private + def build_copilot_response(copilot_message) + if Current.account.usage_limits[:captain][:responses][:current_available].positive? + copilot_message.enqueue_response_job(copilot_thread_params[:conversation_id], Current.user.id) + else + copilot_message.copilot_thread.copilot_messages.create!( + message_type: :assistant, + message: { content: I18n.t('captain.copilot_limit') } + ) + end + end + def ensure_message - return render_could_not_create_error('Message is required') if copilot_thread_params[:message].blank? + return render_could_not_create_error(I18n.t('captain.copilot_message_required')) if copilot_thread_params[:message].blank? end def assistant 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 index b8fc628d1..8b561f22a 100644 --- 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 @@ -86,7 +86,28 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do end context 'with valid params' do + it 'returns error when usage limit is exceeded' do + account.limits = { captain_responses: 2 } + account.custom_attributes = { captain_responses_usage: 2 } + account.save! + + post "/api/v1/accounts/#{account.id}/captain/copilot_threads", + params: valid_params, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + + expect(CopilotMessage.last.message['content']).to eq( + 'You are out of Copilot credits. You can buy more credits from the billing section.' + ) + end + it 'creates a new copilot thread with initial message' do + account.limits = { captain_responses: 2 } + account.custom_attributes = { captain_responses_usage: 0 } + account.save! + expect do post "/api/v1/accounts/#{account.id}/captain/copilot_threads", params: valid_params, @@ -103,8 +124,15 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do expect(thread.assistant_id).to eq(assistant.id) message = thread.copilot_messages.last - expect(message.message_type).to eq('user') expect(message.message).to eq({ 'content' => valid_params[:message] }) + + expect(Captain::Copilot::ResponseJob).to have_been_enqueued.with( + assistant: assistant, + conversation_id: valid_params[:conversation_id], + user_id: agent.id, + copilot_thread_id: thread.id, + message: valid_params[:message] + ) end end end From 24ea968b00eacde7de7173aab959e7ae8a97d08b Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 1 Jul 2025 09:43:44 +0530 Subject: [PATCH 09/31] chore: Remove older UI (#11720) --- app/javascript/dashboard/App.vue | 2 +- .../dashboard/assets/scss/_animations.scss | 101 --- .../assets/scss/{widgets => }/_base.scss | 5 + .../dashboard/assets/scss/_formulate.scss | 38 - .../assets/scss/_helper-classes.scss | 21 - .../dashboard/assets/scss/_layout.scss | 48 -- .../dashboard/assets/scss/_mixins.scss | 101 --- .../dashboard/assets/scss/_rtl.scss | 204 ----- .../dashboard/assets/scss/_variables.scss | 97 --- .../dashboard/assets/scss/_woot.scss | 434 ++-------- .../scss/{ => plugins}/_date-picker.scss | 16 +- .../assets/scss/plugins/_dropdown.scss | 7 - .../assets/scss/plugins/_multiselect.scss | 7 +- .../assets/scss/views/settings/inbox.scss | 1 - .../scss/views/settings/integrations.scss | 1 - .../scss/widgets/_conversation-view.scss | 261 ------ .../dashboard/assets/scss/widgets/_tabs.scss | 77 -- .../assets/scss/widgets/_woot-tables.scss | 90 -- .../LiveChatCampaignDialog.vue | 4 +- .../LiveChatCampaign/LiveChatCampaignForm.vue | 2 +- .../SMSCampaign/SMSCampaignDialog.vue | 4 +- .../SMSCampaign/SMSCampaignForm.vue | 2 +- .../Contacts/ContactsSidebar/ContactMerge.vue | 2 +- .../components-next/Editor/Editor.vue | 2 +- .../components-next/EmptyStateLayout.vue | 4 +- .../ArticleCard/ArticleCard.story.vue | 2 +- .../CategoryCard/CategoryCard.story.vue | 2 +- .../HelpCenter/CategoryCard/CategoryCard.vue | 6 +- .../Article/ArticleEmptyState.story.vue | 2 +- .../Portal/PortalEmptyState.story.vue | 2 +- .../LocaleCard/LocaleCard.story.vue | 2 +- .../HelpCenter/LocaleCard/LocaleCard.vue | 14 +- .../Pages/ArticleEditorPage/ArticleEditor.vue | 8 +- .../ArticleEditorControls.vue | 4 +- .../ArticleEditorPage/ArticleEditorHeader.vue | 2 +- .../Pages/ArticlePage/ArticlesPage.story.vue | 2 +- .../CategoryPage/CategoriesPage.story.vue | 2 +- .../Pages/CategoryPage/CategoryDialog.vue | 4 +- .../Pages/CategoryPage/CategoryForm.vue | 2 +- .../CategoryPage/CategoryHeaderControls.vue | 6 +- .../Pages/LocalePage/LocalesPage.story.vue | 2 +- .../Pages/LocalePage/LocalesPage.vue | 2 +- .../PortalSettingsPage/PortalBaseSettings.vue | 14 +- .../PortalSettings.story.vue | 2 +- .../PortalSettingsPage/PortalSettings.vue | 4 +- .../PortalSwitcher/PortalSwitcher.story.vue | 2 +- .../PortalSwitcher/PortalSwitcher.vue | 4 +- .../components-next/avatar/Avatar.story.vue | 14 +- .../breadcrumb/Breadcrumb.story.vue | 10 +- .../components-next/breadcrumb/Breadcrumb.vue | 4 +- .../components-next/button/Button.story.vue | 20 +- .../button/ConfirmButton.story.vue | 4 +- .../captain/assistant/AssistantCard.story.vue | 2 +- .../captain/assistant/DocumentCard.story.vue | 2 +- .../captain/assistant/InboxCard.story.vue | 2 +- .../captain/assistant/ResponseCard.story.vue | 2 +- .../assistant/AssistantForm.vue | 2 +- .../pageComponents/document/DocumentForm.vue | 2 +- .../pageComponents/inbox/ConnectInboxForm.vue | 2 +- .../pageComponents/response/ResponseForm.vue | 2 +- .../colorpicker/ColorPicker.vue | 2 +- .../combobox/ComboBox.story.vue | 4 +- .../combobox/ComboBoxDropdown.vue | 7 +- .../TagMultiSelectComboxBox.story.vue | 8 +- .../copilot/CopilotLauncher.vue | 5 +- .../dropdown-menu/DropdownMenu.story.vue | 8 +- .../DropdownPrimitives.story.vue | 2 +- .../inline-input/InlineInput.story.vue | 14 +- .../components-next/input/Input.story.vue | 14 +- .../dashboard/components-next/input/Input.vue | 2 +- .../components-next/message/bubbles/Dyte.vue | 10 +- .../components-next/message/chips/File.vue | 2 +- .../pagination/PaginationFooter.story.vue | 10 +- .../sidebar/SidebarGroupHeader.vue | 3 +- .../sidebar/SidebarGroupLeaf.vue | 2 +- .../sidebar/SidebarProfileMenu.vue | 6 +- .../components-next/switch/Switch.vue | 6 +- .../components-next/tabbar/TabBar.story.vue | 8 +- .../textarea/TextArea.story.vue | 14 +- .../components-next/textarea/TextArea.vue | 4 +- .../dashboard/components/ChatList.vue | 7 +- .../dashboard/components/ChatListHeader.vue | 15 - .../dashboard/components/FormSection.vue | 6 +- .../dashboard/components/ModalHeader.vue | 6 +- .../dashboard/components/SettingsSection.vue | 9 +- .../dashboard/components/SidemenuIcon.vue | 49 -- .../AddAccountModal.vue | 0 .../components/buttons/ResolveAction.vue | 10 +- .../components/copilot/CopilotContainer.vue | 2 +- app/javascript/dashboard/components/index.js | 2 - .../components/layout/AvailabilityStatus.vue | 157 ---- .../dashboard/components/layout/Sidebar.vue | 246 ------ .../layout/config/default-sidebar.js | 19 - .../layout/config/sidebarItems/campaigns.js | 26 - .../layout/config/sidebarItems/contacts.js | 32 - .../config/sidebarItems/conversations.js | 50 -- .../config/sidebarItems/notifications.js | 7 - .../layout/config/sidebarItems/primaryMenu.js | 70 -- .../config/sidebarItems/profileSettings.js | 7 - .../layout/config/sidebarItems/reports.js | 87 -- .../layout/config/sidebarItems/settings.js | 220 ----- .../sidebarComponents/AccountContext.vue | 110 --- .../sidebarComponents/AccountSelector.vue | 88 -- .../layout/sidebarComponents/AgentDetails.vue | 45 - .../layout/sidebarComponents/Logo.vue | 33 - .../sidebarComponents/NotificationBell.vue | 58 -- .../layout/sidebarComponents/OptionsMenu.vue | 204 ----- .../layout/sidebarComponents/Primary.vue | 116 --- .../sidebarComponents/PrimaryNavItem.vue | 148 ---- .../layout/sidebarComponents/Secondary.vue | 265 ------ .../SecondaryChildNavItem.vue | 132 --- .../sidebarComponents/SecondaryNavItem.vue | 277 ------ .../specs/AccountSelector.spec.js | 73 -- .../specs/AgentDetails.spec.js | 61 -- .../specs/NotificationBell.spec.js | 88 -- .../layout/specs/AvailabilityStatus.spec.js | 71 -- .../components/specs/SidemenuIcon.spec.js | 31 - .../__snapshots__/SidemenuIcon.spec.js.snap | 14 - .../dashboard/components/table/BaseCell.vue | 2 +- .../dashboard/components/ui/Banner.vue | 12 +- .../components/CalendarDateInput.vue | 2 +- .../components/DatePickerButton.vue | 16 +- .../components/ui/HelperTextPopup.vue | 6 +- .../dashboard/components/ui/Label.vue | 42 +- .../dashboard/components/ui/PreviewCard.vue | 23 +- .../dashboard/components/ui/Switch.vue | 21 +- .../dashboard/components/ui/Tabs/Tabs.vue | 23 +- .../dashboard/components/ui/Tabs/TabsItem.vue | 30 +- .../dashboard/components/ui/TimeAgo.vue | 2 +- .../dashboard/components/ui/Wizard.vue | 13 +- .../widgets/AIAssistanceCTAButton.vue | 4 +- .../components/widgets/AIAssistanceModal.vue | 4 +- .../dashboard/components/widgets/AILoader.vue | 14 +- .../components/widgets/AttachmentsPreview.vue | 2 +- .../widgets/AutomationActionInput.vue | 4 +- .../AutomationActionTeamMessageInput.vue | 4 +- .../widgets/AutomationFileInput.vue | 6 +- .../dashboard/components/widgets/Avatar.vue | 2 +- .../components/widgets/ChatTypeTabs.vue | 15 +- .../components/widgets/ColorPicker.vue | 7 +- .../components/widgets/EmptyState.vue | 2 +- .../components/widgets/FilterInput/Index.vue | 8 +- .../components/widgets/LoadingState.vue | 18 +- .../components/widgets/SettingIntroBanner.vue | 7 +- .../dashboard/components/widgets/ShowMore.vue | 2 +- .../components/widgets/TableFooterResults.vue | 2 +- .../components/widgets/Thumbnail.vue | 24 +- .../components/widgets/ThumbnailGroup.vue | 22 +- .../components/widgets/WootWriter/Editor.vue | 18 +- .../widgets/WootWriter/ReplyBottomPanel.vue | 6 +- .../widgets/WootWriter/ReplyTopPanel.vue | 2 +- .../WootWriter/keyboardEmojiSelector.vue | 4 +- .../conversation/AvailabilityStatusBadge.vue | 27 - .../widgets/conversation/ChatFilter.vue | 1 - .../widgets/conversation/ConversationBox.vue | 22 +- .../widgets/conversation/ConversationCard.vue | 6 +- .../EmptyState/EmptyStateMessage.vue | 4 +- .../widgets/conversation/Message.vue | 800 ------------------ .../widgets/conversation/MessagePreview.vue | 10 +- .../MessageSignatureMissingAlert.vue | 2 +- .../widgets/conversation/MessagesView.vue | 150 +--- .../conversation/OnboardingFeatureCard.vue | 8 +- .../widgets/conversation/OnboardingView.vue | 4 +- .../widgets/conversation/ReplyBox.vue | 60 +- .../widgets/conversation/ShopifyOrderItem.vue | 10 +- .../widgets/conversation/TagAgents.vue | 2 +- .../widgets/conversation/VariableList.vue | 2 +- .../widgets/conversation/bubble/Actions.vue | 357 -------- .../widgets/conversation/bubble/Contact.vue | 123 --- .../widgets/conversation/bubble/File.vue | 86 -- .../widgets/conversation/bubble/Image.vue | 34 - .../conversation/bubble/ImageAudioVideo.vue | 127 --- .../conversation/bubble/InstagramStory.vue | 53 -- .../bubble/InstagramStoryErrorPlaceHolder.vue | 14 - .../bubble/InstagramStoryReply.vue | 36 - .../conversation/bubble/Integration.vue | 41 - .../widgets/conversation/bubble/Location.vue | 52 -- .../widgets/conversation/bubble/MailHead.vue | 100 --- .../widgets/conversation/bubble/ReplyTo.vue | 57 -- .../widgets/conversation/bubble/Text.vue | 162 ---- .../widgets/conversation/bubble/Video.vue | 43 - .../conversation/bubble/integrations/Dyte.vue | 98 --- .../contextMenu/agentLoadingPlaceholder.vue | 6 +- .../conversation/contextMenu/menuItem.vue | 9 +- .../contextMenu/menuItemWithSubmenu.vue | 2 +- .../conversation/LabelSuggestion.vue | 21 +- .../conversationBulkActions/AgentSelector.vue | 11 +- .../conversationBulkActions/Index.vue | 11 +- .../conversationBulkActions/LabelActions.vue | 21 +- .../conversationBulkActions/TeamActions.vue | 15 +- .../conversationBulkActions/UpdateActions.vue | 6 +- .../conversationCardComponents/CardLabels.vue | 2 +- .../helpers/botMessageContentHelper.js | 87 -- .../specs/botMessageContentHelper.spec.js | 66 -- .../conversation/linear/CreateOrLinkIssue.vue | 3 +- .../widgets/forms/AvatarUploader.vue | 9 +- .../components/widgets/forms/PhoneInput.vue | 2 +- .../widgets/mentions/MentionBox.vue | 4 +- .../widgets/modal/WootKeyShortcutModal.vue | 8 +- .../dashboard/helper/permissionsHelper.js | 26 - .../dashboard/helper/portalHelper.js | 36 - .../helper/specs/permissionsHelper.spec.js | 65 -- .../components/ContactDropdownItem.vue | 6 +- .../contact/components/MergeContact.vue | 2 +- .../components/MergeContactSummary.vue | 4 +- .../components/MessageContextMenu.vue | 6 +- .../modules/search/components/SearchTabs.vue | 1 + .../widget-preview/components/Widget.vue | 4 +- .../widget-preview/components/WidgetBody.vue | 2 +- .../components/WidgetFooter.vue | 14 +- .../widget-preview/components/WidgetHead.vue | 12 +- .../dashboard/routes/dashboard/Dashboard.vue | 92 +- .../routes/dashboard/commands/commandbar.vue | 2 +- .../conversation/ContactConversations.vue | 2 +- .../conversation/ContactDetailsItem.vue | 2 +- .../dashboard/conversation/ContactPanel.vue | 2 +- .../conversation/ConversationAction.vue | 2 +- .../conversation/ConversationParticipant.vue | 24 +- .../conversation/ConversationView.vue | 18 +- .../conversation/Macros/MacroPreview.vue | 4 +- .../conversation/contact/ContactForm.vue | 6 +- .../conversation/contact/SocialIcons.vue | 2 +- .../conversation/labels/LabelBox.vue | 51 +- .../conversation/search/PopOverSearch.vue | 78 -- .../ArticleSearch/ArticleSearchResultItem.vue | 6 +- .../components/ArticleSearch/Header.vue | 4 +- .../ArticleSearch/SearchPopover.vue | 2 +- .../ArticleSearch/SearchResults.vue | 18 +- .../helpcenter/components/UpgradePage.vue | 12 +- .../pages/HelpCenterPageRouteView.vue | 2 +- .../helpcenter/pages/PortalsIndexPage.vue | 2 +- .../dashboard/inbox/InboxEmptyState.vue | 17 +- .../routes/dashboard/inbox/InboxList.vue | 8 +- .../routes/dashboard/inbox/InboxView.vue | 6 +- .../inbox/components/InboxDisplayMenu.vue | 4 +- .../routes/dashboard/inbox/routes.js | 4 - .../components/NotificationPanel.vue | 230 ----- .../components/NotificationPanelItem.vue | 106 --- .../components/NotificationPanelList.vue | 81 -- .../components/NotificationTable.vue | 23 +- .../components/NotificationsView.vue | 16 +- .../routes/dashboard/notifications/routes.js | 1 - .../dashboard/settings/SettingsHeader.vue | 7 +- .../dashboard/settings/SettingsLayout.vue | 2 +- .../settings/SettingsSubPageHeader.vue | 6 +- .../routes/dashboard/settings/Wrapper.vue | 2 - .../account/components/AccountDelete.vue | 4 +- .../dashboard/settings/agents/Index.vue | 4 +- .../settings/attributes/AddAttribute.vue | 25 +- .../settings/attributes/CustomAttribute.vue | 4 +- .../settings/attributes/EditAttribute.vue | 25 +- .../dashboard/settings/attributes/Index.vue | 3 +- .../dashboard/settings/auditlogs/Index.vue | 6 +- .../settings/automation/AddAutomationRule.vue | 2 +- .../automation/EditAutomationRule.vue | 2 +- .../dashboard/settings/automation/Index.vue | 2 +- .../dashboard/settings/canned/Index.vue | 7 +- .../components/BaseSettingsHeader.vue | 4 +- .../components/BaseSettingsListItem.vue | 6 +- .../dashboard/settings/customRoles/Index.vue | 7 +- .../customRoles/component/CustomRoleModal.vue | 2 +- .../component/CustomRolePaywall.vue | 2 +- .../dashboard/settings/inbox/FinishSetup.vue | 4 +- .../routes/dashboard/settings/inbox/Index.vue | 4 +- .../inbox/PreChatForm/PreChatFields.vue | 6 +- .../settings/inbox/PreChatForm/Settings.vue | 6 +- .../dashboard/settings/inbox/Settings.vue | 11 +- .../settings/inbox/WidgetBuilder.vue | 2 +- .../settings/inbox/channels/Instagram.vue | 2 +- .../settings/inbox/components/BusinessDay.vue | 4 +- .../inbox/components/InputRadioGroup.vue | 8 +- .../components/SenderNameExamplePreview.vue | 2 +- .../inbox/components/WeeklyAvailability.vue | 2 +- .../settings/inbox/facebook/Reauthorize.vue | 4 +- .../inbox/settingsPage/CollaboratorsPage.vue | 15 +- .../integrations/DashboardApps/Index.vue | 8 +- .../integrations/MultipleIntegrationHooks.vue | 12 +- .../settings/integrations/NewHook.vue | 2 +- .../settings/integrations/Shopify.vue | 2 +- .../Slack/SelectChannelWarning.vue | 2 +- .../settings/integrations/Webhooks/Index.vue | 6 +- .../integrations/Webhooks/WebhookRow.vue | 4 +- .../dashboard/settings/labels/Index.vue | 16 +- .../dashboard/settings/macros/Index.vue | 2 +- .../dashboard/settings/macros/MacroNode.vue | 4 +- .../dashboard/settings/macros/MacroNodes.vue | 17 +- .../settings/macros/MacroProperties.vue | 24 +- .../settings/profile/AccessToken.vue | 2 +- .../settings/profile/AudioAlertCondition.vue | 4 +- .../settings/profile/AudioAlertEvent.vue | 4 +- .../settings/profile/AudioAlertTone.vue | 2 +- .../settings/profile/ChangePassword.vue | 20 +- .../dashboard/settings/profile/HotKeyCard.vue | 10 +- .../dashboard/settings/profile/Index.vue | 2 +- .../settings/profile/MessageSignature.vue | 16 +- .../settings/profile/NotificationCheckBox.vue | 2 +- .../profile/NotificationPreferences.vue | 26 +- .../settings/profile/UserBasicDetails.vue | 15 +- .../settings/profile/UserProfilePicture.vue | 2 +- .../dashboard/settings/profile/Wrapper.vue | 2 +- .../settings/reports/ReportContainer.vue | 2 +- .../components/ChartElements/ChartStats.vue | 8 +- .../reports/components/ConversationCell.vue | 5 +- .../settings/reports/components/CsatTable.vue | 2 +- .../components/Filters/DateGroupBy.vue | 2 +- .../reports/components/Filters/Labels.vue | 6 +- .../settings/reports/components/Heatmap.vue | 30 +- .../reports/components/ReportFilters.vue | 10 +- .../reports/components/SLA/SLAFilter.vue | 7 +- .../reports/components/SLA/SLAReportItem.vue | 6 +- .../reports/components/SLA/SLAViewDetails.vue | 2 +- .../reports/components/overview/AgentCell.vue | 4 +- .../routes/dashboard/settings/sla/SlaForm.vue | 2 +- .../sla/components/SLABusinessHoursLabel.vue | 14 +- .../settings/sla/components/SLAListItem.vue | 4 +- .../sla/components/SLAListItemLoading.vue | 12 +- .../sla/components/SLAResponseTime.vue | 6 +- .../settings/teams/AgentSelector.vue | 13 +- .../settings/teams/Create/AddAgents.vue | 2 +- .../settings/teams/Edit/EditAgents.vue | 2 +- .../routes/dashboard/settings/teams/Index.vue | 9 +- .../routes/dashboard/suspended/Index.vue | 2 +- app/javascript/dashboard/routes/index.js | 2 - .../shared/assets/stylesheets/animations.scss | 11 - .../assets/stylesheets/border-radius.scss | 8 - .../shared/assets/stylesheets/colors.scss | 109 --- .../shared/assets/stylesheets/font-size.scss | 14 - .../assets/stylesheets/font-weights.scss | 8 - .../shared/assets/stylesheets/shadows.scss | 19 - .../shared/assets/stylesheets/spacing.scss | 33 - .../shared/assets/stylesheets/z-index.scss | 14 - .../components/ArticleSkeletonLoader.vue | 30 +- app/javascript/shared/components/Button.vue | 4 +- app/javascript/shared/components/Spinner.vue | 6 +- app/javascript/shared/components/TextArea.vue | 13 +- .../shared/components/emoji/EmojiInput.vue | 17 +- .../components/ui/MultiselectDropdown.vue | 13 +- .../ui/MultiselectDropdownItems.vue | 4 +- .../ui/dropdown/DropdownDivider.vue | 2 +- .../components/ui/dropdown/DropdownHeader.vue | 2 +- .../ui/dropdown/DropdownSubMenu.vue | 14 +- .../components/ui/label/LabelDropdown.vue | 8 +- app/javascript/shared/constants/busEvents.js | 1 - app/javascript/shared/helpers/DateHelper.js | 11 +- .../shared/helpers/specs/DateHelper.spec.js | 32 - app/javascript/v3/App.vue | 6 +- .../v3/components/Button/SubmitButton.vue | 65 -- app/javascript/v3/components/Form/Button.vue | 101 --- .../v3/components/Form/CheckBox.vue | 2 +- .../v3/components/Form/InitialsAvatar.vue | 43 - app/javascript/v3/components/Form/Switch.vue | 6 +- .../v3/components/Form/Textarea.vue | 85 -- .../v3/components/SnackBar/Item.vue | 2 +- .../v3/views/auth/password/Edit.vue | 14 +- .../v3/views/auth/reset/password/Index.vue | 14 +- .../auth/signup/components/Signup/Form.vue | 24 +- .../signup/components/Testimonials/Index.vue | 2 +- app/javascript/v3/views/login/Index.vue | 16 +- .../v3/views/onboarding/OnboardingStep.vue | 14 +- app/javascript/widget/assets/scss/woot.scss | 3 - .../widget/components/ChatHeader.vue | 2 +- .../widget/components/FooterReplyTo.vue | 4 +- .../widget/components/MessageReplyButton.vue | 2 +- .../widget/components/ReplyToChip.vue | 6 +- .../widget/components/UnreadMessageList.vue | 4 +- .../widget/components/template/Article.vue | 2 +- .../account_features_field/_form.html.erb | 4 +- .../_form.html.erb | 2 +- theme/colors.js | 182 +--- 369 files changed, 974 insertions(+), 9363 deletions(-) delete mode 100644 app/javascript/dashboard/assets/scss/_animations.scss rename app/javascript/dashboard/assets/scss/{widgets => }/_base.scss (98%) delete mode 100644 app/javascript/dashboard/assets/scss/_formulate.scss delete mode 100644 app/javascript/dashboard/assets/scss/_helper-classes.scss delete mode 100644 app/javascript/dashboard/assets/scss/_layout.scss delete mode 100644 app/javascript/dashboard/assets/scss/_mixins.scss delete mode 100644 app/javascript/dashboard/assets/scss/_rtl.scss delete mode 100644 app/javascript/dashboard/assets/scss/_variables.scss rename app/javascript/dashboard/assets/scss/{ => plugins}/_date-picker.scss (73%) delete mode 100644 app/javascript/dashboard/assets/scss/plugins/_dropdown.scss delete mode 100644 app/javascript/dashboard/assets/scss/views/settings/inbox.scss delete mode 100644 app/javascript/dashboard/assets/scss/views/settings/integrations.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_tabs.scss delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_woot-tables.scss delete mode 100644 app/javascript/dashboard/components/SidemenuIcon.vue rename app/javascript/dashboard/components/{layout/sidebarComponents => app}/AddAccountModal.vue (100%) delete mode 100644 app/javascript/dashboard/components/layout/AvailabilityStatus.vue delete mode 100644 app/javascript/dashboard/components/layout/Sidebar.vue delete mode 100644 app/javascript/dashboard/components/layout/config/default-sidebar.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/campaigns.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/contacts.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/conversations.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/notifications.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/profileSettings.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/reports.js delete mode 100644 app/javascript/dashboard/components/layout/config/sidebarItems/settings.js delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/AccountContext.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/AgentDetails.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/Logo.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/NotificationBell.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/PrimaryNavItem.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/SecondaryChildNavItem.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/specs/AccountSelector.spec.js delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/specs/AgentDetails.spec.js delete mode 100644 app/javascript/dashboard/components/layout/sidebarComponents/specs/NotificationBell.spec.js delete mode 100644 app/javascript/dashboard/components/layout/specs/AvailabilityStatus.spec.js delete mode 100644 app/javascript/dashboard/components/specs/SidemenuIcon.spec.js delete mode 100644 app/javascript/dashboard/components/specs/__snapshots__/SidemenuIcon.spec.js.snap delete mode 100644 app/javascript/dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/ChatFilter.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/Message.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Contact.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/File.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Image.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/ImageAudioVideo.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/InstagramStory.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/InstagramStoryErrorPlaceHolder.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/InstagramStoryReply.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Integration.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Location.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/MailHead.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/ReplyTo.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Text.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/Video.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/integrations/Dyte.vue delete mode 100644 app/javascript/dashboard/components/widgets/conversation/helpers/botMessageContentHelper.js delete mode 100644 app/javascript/dashboard/components/widgets/conversation/helpers/specs/botMessageContentHelper.spec.js delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelItem.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelList.vue delete mode 100644 app/javascript/shared/assets/stylesheets/animations.scss delete mode 100644 app/javascript/shared/assets/stylesheets/border-radius.scss delete mode 100644 app/javascript/shared/assets/stylesheets/colors.scss delete mode 100644 app/javascript/shared/assets/stylesheets/font-size.scss delete mode 100644 app/javascript/shared/assets/stylesheets/font-weights.scss delete mode 100644 app/javascript/shared/assets/stylesheets/shadows.scss delete mode 100644 app/javascript/shared/assets/stylesheets/spacing.scss delete mode 100644 app/javascript/shared/assets/stylesheets/z-index.scss delete mode 100644 app/javascript/v3/components/Button/SubmitButton.vue delete mode 100644 app/javascript/v3/components/Form/Button.vue delete mode 100644 app/javascript/v3/components/Form/InitialsAvatar.vue delete mode 100644 app/javascript/v3/components/Form/Textarea.vue diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index e51958e9e..675f6ea67 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -1,6 +1,6 @@