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]: () => {