From 5491ca24701975a60c6b38532098ab153115e397 Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 5 Nov 2025 11:42:21 -0800 Subject: [PATCH 1/4] feat: Differentiate bot and user in the summary (#12801) While generating the summary, use the appropriate sender type for the message. --- .../llm_formatter/conversation_llm_formatter.rb | 9 ++++++++- spec/factories/messages.rb | 7 +++++++ .../llm_formatter/conversation_llm_formatter_spec.rb | 11 ++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/services/llm_formatter/conversation_llm_formatter.rb b/app/services/llm_formatter/conversation_llm_formatter.rb index 8654e0adf..4e0bd7013 100644 --- a/app/services/llm_formatter/conversation_llm_formatter.rb +++ b/app/services/llm_formatter/conversation_llm_formatter.rb @@ -39,7 +39,14 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter end def format_message(message) - sender = message.message_type == 'incoming' ? 'User' : 'Support agent' + sender = case message.sender_type + when 'User' + 'Support Agent' + when 'Contact' + 'User' + else + 'Bot' + end sender = "[Private Note] #{sender}" if message.private? "#{sender}: #{message.content}\n" end diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb index b2ae41c5e..99a2c7cd8 100644 --- a/spec/factories/messages.rb +++ b/spec/factories/messages.rb @@ -27,6 +27,13 @@ FactoryBot.define do end end + trait :bot_message do + message_type { 'outgoing' } + after(:build) do |message| + message.sender = nil + end + end + after(:build) do |message| message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account) message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account) diff --git a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb index 49fcc1a18..b79ee6d79 100644 --- a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb +++ b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb @@ -28,6 +28,14 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do content: 'Hello, I need help' ) + create( + :message, + :bot_message, + conversation: conversation, + message_type: 'outgoing', + content: 'Thanks for reaching out, an agent will reach out to you soon' + ) + create( :message, conversation: conversation, @@ -40,7 +48,8 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do "Channel: #{conversation.inbox.channel.name}", 'Message History:', 'User: Hello, I need help', - 'Support agent: How can I assist you today?', + 'Bot: Thanks for reaching out, an agent will reach out to you soon', + 'Support Agent: How can I assist you today?', '' ].join("\n") From 48ba273730f57aafc7df67e6f70154549dff8cf7 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:53:31 +0530 Subject: [PATCH 2/4] fix: Invalid image URL issue in Help Center articles (#12806) --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6fb7156cb..4660b44fa 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", - "@chatwoot/prosemirror-schema": "1.2.1", + "@chatwoot/prosemirror-schema": "1.2.3", "@chatwoot/utils": "^0.0.51", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22c662d89..e5629af3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,8 +20,8 @@ importers: specifier: 1.2.3 version: 1.2.3 '@chatwoot/prosemirror-schema': - specifier: 1.2.1 - version: 1.2.1 + specifier: 1.2.3 + version: 1.2.3 '@chatwoot/utils': specifier: ^0.0.51 version: 0.0.51 @@ -406,8 +406,8 @@ packages: '@chatwoot/ninja-keys@1.2.3': resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==} - '@chatwoot/prosemirror-schema@1.2.1': - resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==} + '@chatwoot/prosemirror-schema@1.2.3': + resolution: {integrity: sha512-q/EfirVK9jt8FJAx3Gf6y3LoVadmYVLknbYvPrkUe81WO0f2mkZ/kY2UQgpUISVvOGEkCH4bkfYMp5UQ+Buz3g==} '@chatwoot/utils@0.0.51': resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==} @@ -4768,7 +4768,7 @@ snapshots: hotkeys-js: 3.8.7 lit: 2.2.6 - '@chatwoot/prosemirror-schema@1.2.1': + '@chatwoot/prosemirror-schema@1.2.3': dependencies: markdown-it-sup: 2.0.0 prosemirror-commands: 1.6.0 From ec6c3b3571187e2e3f64781746d9e5a81e046dee Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 6 Nov 2025 14:00:47 +0530 Subject: [PATCH 3/4] feat: allow bots to handle campaigns when sender_id is nil (#12805) --- app/models/conversation.rb | 9 +++++--- spec/models/conversation_spec.rb | 35 +++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 4ec63acc2..eb97a22e8 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -228,14 +228,17 @@ class Conversation < ApplicationRecord def determine_conversation_status self.status = :resolved and return if contact.blocked? - # Message template hooks aren't executed for conversations from campaigns - # So making these conversations open for agent visibility - return if campaign.present? + return handle_campaign_status if campaign.present? # TODO: make this an inbox config instead of assuming bot conversations should start as pending self.status = :pending if inbox.active_bot? end + def handle_campaign_status + # If campaign has no sender (bot-initiated) and inbox has active bot, let bot handle it + self.status = :pending if campaign.sender_id.nil? && inbox.active_bot? + end + def notify_conversation_creation dispatcher_dispatch(CONVERSATION_CREATED) end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 72962ba54..51bb43384 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -576,9 +576,38 @@ RSpec.describe Conversation do expect(conversation.status).to eq('pending') end - it 'returns conversation as open if campaign is present' do - conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: create(:campaign)) - expect(conversation.status).to eq('open') + context 'with campaigns' do + let(:user) { create(:user, account: bot_inbox.inbox.account) } + + it 'returns conversation as open if campaign has a sender' do + campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: user) + conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign) + expect(conversation.status).to eq('open') + end + + it 'returns conversation as pending if campaign has no sender (bot-initiated) and bot is active' do + campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: nil) + conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign) + expect(conversation.status).to eq('pending') + end + end + + context 'with campaigns in inbox without bot' do + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:user) { create(:user, account: account) } + + it 'returns conversation as open if campaign has no sender but no bot is active' do + campaign = create(:campaign, inbox: inbox, account: account, sender: nil) + conversation = create(:conversation, inbox: inbox, campaign: campaign) + expect(conversation.status).to eq('open') + end + + it 'returns conversation as open if campaign has a sender' do + campaign = create(:campaign, inbox: inbox, account: account, sender: user) + conversation = create(:conversation, inbox: inbox, campaign: campaign) + expect(conversation.status).to eq('open') + end end end From 9b75d9bd1b21341c6af2024b24d743434c8c6879 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:05:52 +0530 Subject: [PATCH 4/4] fix: Add empty line before signature in compose conversation editor (#12702) Co-authored-by: Shivam Mishra --- .../components-next/Editor/Editor.vue | 8 +++++++ .../components/ActionButtons.vue | 1 - .../components/ComposeNewConversationForm.vue | 22 ++++++++++++------- .../components/MessageEditor.vue | 5 +++++ .../components/widgets/WootWriter/Editor.vue | 11 +++++++++- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue index a2f139bdc..67936fa59 100644 --- a/app/javascript/dashboard/components-next/Editor/Editor.vue +++ b/app/javascript/dashboard/components-next/Editor/Editor.vue @@ -21,6 +21,10 @@ const props = defineProps({ enableCannedResponses: { type: Boolean, default: true }, enabledMenuOptions: { type: Array, default: () => [] }, enableCaptainTools: { type: Boolean, default: false }, + signature: { type: String, default: '' }, + allowSignature: { type: Boolean, default: false }, + sendWithSignature: { type: Boolean, default: false }, + channelType: { type: String, default: '' }, }); const emit = defineEmits(['update:modelValue']); @@ -100,6 +104,10 @@ watch( :enable-canned-responses="enableCannedResponses" :enabled-menu-options="enabledMenuOptions" :enable-captain-tools="enableCaptainTools" + :signature="signature" + :allow-signature="allowSignature" + :send-with-signature="sendWithSignature" + :channel-type="channelType" @input="handleInput" @focus="handleFocus" @blur="handleBlur" diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 773ebe315..92c5850de 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -92,7 +92,6 @@ const setSignature = () => { const toggleMessageSignature = () => { setSignatureFlagForInbox(props.channelType, !sendWithSignature.value); - setSignature(); }; // Added this watch to dynamically set signature on target inbox change. diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue index 4d6d41dac..a02d6d495 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue @@ -199,16 +199,20 @@ const handleInboxAction = ({ value, action, ...rest }) => { state.attachedFiles = []; }; -const removeTargetInbox = value => { - v$.value.$reset(); - // Remove the signature from message content - // Based on the Advance Editor (used in isEmailOrWebWidget) and Plain editor(all other inboxes except WhatsApp) - if (props.sendWithSignature) { - const signatureToRemove = inboxTypes.value.isEmailOrWebWidget - ? props.messageSignature - : extractTextFromMarkdown(props.messageSignature); +const removeSignatureFromMessage = () => { + // Always remove the signature from message content when inbox/contact is removed + // to ensure no leftover signature content remains + const signatureToRemove = inboxTypes.value.isEmailOrWebWidget + ? props.messageSignature + : extractTextFromMarkdown(props.messageSignature); + if (signatureToRemove) { state.message = removeSignature(state.message, signatureToRemove); } +}; + +const removeTargetInbox = value => { + v$.value.$reset(); + removeSignatureFromMessage(); emit('updateTargetInbox', value); state.attachedFiles = []; }; @@ -216,6 +220,7 @@ const removeTargetInbox = value => { const clearSelectedContact = () => { emit('clearSelectedContact'); state.attachedFiles = []; + removeSignatureFromMessage(); }; const onClickInsertEmoji = emoji => { @@ -354,6 +359,7 @@ const shouldShowMessageEditor = computed(() => { :is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget" :has-errors="validationStates.isMessageInvalid" :has-attachments="state.attachedFiles.length > 0" + :channel-type="inboxChannelType" /> { " enable-variables :show-character-count="false" + :signature="messageSignature" + allow-signature + :send-with-signature="sendWithSignature" + :channel-type="channelType" />