diff --git a/frontend/apps/main/src/features/admin/inbox/livechatFormSchema.js b/frontend/apps/main/src/features/admin/inbox/livechatFormSchema.js
index 3c5e3f23..49ed33ff 100644
--- a/frontend/apps/main/src/features/admin/inbox/livechatFormSchema.js
+++ b/frontend/apps/main/src/features/admin/inbox/livechatFormSchema.js
@@ -33,6 +33,7 @@ export const createFormSchema = (t) => z.object({
chat_introduction: z.string(),
show_office_hours_in_chat: z.boolean(),
show_office_hours_after_assignment: z.boolean(),
+ chat_reply_expectation_message: z.string().optional(),
notice_banner: z.object({
enabled: z.boolean(),
text: z.string().optional()
diff --git a/frontend/apps/widget/src/App.vue b/frontend/apps/widget/src/App.vue
index 1eb3526c..b02c2405 100644
--- a/frontend/apps/widget/src/App.vue
+++ b/frontend/apps/widget/src/App.vue
@@ -2,6 +2,8 @@
@@ -16,6 +18,7 @@ import { useChatStore } from '@widget/store/chat.js'
import { useUserStore } from './store/user.js'
import { initWidgetWS, closeWidgetWebSocket } from './websocket.js'
import { useUnreadCount } from './composables/useUnreadCount.js'
+import { initAudioContext } from './composables/useNotificationSound.js'
import { applyCSSColor } from '@shared-ui/utils/color.js'
import MainLayout from '@widget/layouts/MainLayout.vue'
diff --git a/frontend/apps/widget/src/assets/notification.mp3 b/frontend/apps/widget/src/assets/notification.mp3
new file mode 100644
index 00000000..0430b396
Binary files /dev/null and b/frontend/apps/widget/src/assets/notification.mp3 differ
diff --git a/frontend/apps/widget/src/composables/useNotificationSound.js b/frontend/apps/widget/src/composables/useNotificationSound.js
new file mode 100644
index 00000000..96454f43
--- /dev/null
+++ b/frontend/apps/widget/src/composables/useNotificationSound.js
@@ -0,0 +1,17 @@
+import notificationSound from '../assets/notification.mp3'
+
+let audio = null
+
+// Call on first user interaction to unlock audio
+export function initAudioContext() {
+ if (audio) return
+ audio = new Audio(notificationSound)
+ audio.volume = 0.5
+ audio.load()
+}
+
+export function playNotificationSound() {
+ if (!audio) return
+ audio.currentTime = 0
+ audio.play().catch(() => {})
+}
diff --git a/frontend/apps/widget/src/websocket.js b/frontend/apps/widget/src/websocket.js
index 5153d52a..c8945b79 100644
--- a/frontend/apps/widget/src/websocket.js
+++ b/frontend/apps/widget/src/websocket.js
@@ -1,5 +1,8 @@
// Widget WebSocket message types (matching backend constants)
import { useChatStore } from './store/chat.js'
+import { useWidgetStore } from './store/widget.js'
+import { useUserStore } from './store/user.js'
+import { playNotificationSound } from './composables/useNotificationSound.js'
export const WS_EVENT = {
JOIN: 'join',
@@ -83,8 +86,21 @@ export class WidgetWebSocketClient {
this.lastPong = Date.now()
},
[WS_EVENT.NEW_MESSAGE]: () => {
- if (data.data) {
- chatStore.addMessageToConversation(data.data.conversation_uuid, data.data)
+ if (!data.data) return
+
+ const message = data.data
+ chatStore.addMessageToConversation(message.conversation_uuid, message)
+
+ // Play notification sound if message is from agent and widget is not focused on this conversation
+ const widgetStore = useWidgetStore()
+ const userStore = useUserStore()
+ const isFromAgent = message.author?.id !== userStore.userID
+ const isViewingConversation = widgetStore.isOpen &&
+ widgetStore.isInChatView &&
+ chatStore.currentConversation?.uuid === message.conversation_uuid
+
+ if (isFromAgent && !isViewingConversation) {
+ playNotificationSound()
}
},
[WS_EVENT.ERROR]: () => {