-
+
{getSenderName()}
diff --git a/docs/socket-logger.md b/docs/socket-logger.md
new file mode 100644
index 00000000..f6f894b5
--- /dev/null
+++ b/docs/socket-logger.md
@@ -0,0 +1,182 @@
+# Socket Logger (Dev-Only Realtime Traffic Inspector)
+
+A lightweight, dependency-free runtime logger that surfaces every realtime frame
+the chat widget sends or receives — WebSocket, EventSource (SSE), `fetch`,
+`XMLHttpRequest`, and parent ↔ iframe `postMessage`.
+
+It is **automatically disabled in production** and is a true no-op there (no
+global mutation, no console output, zero overhead).
+
+---
+
+## Where it lives
+
+```
+components/Chatbot/utils/socketLogger.ts
+```
+
+It is installed at **module load** (not in a React effect) from
+`components/Chatbot/Chatbot.tsx`, so the wrapper is in place before any
+child code can fire a fetch / XHR / postMessage:
+
+```ts
+// Chatbot.tsx — top of the file
+import { installSocketLogger } from './utils/socketLogger';
+
+// Install the dev-only realtime logger at module load (before any fetch/axios
+// call can fire). The function is a true no-op in production.
+installSocketLogger();
+```
+
+This guarantees that even the first axios call inside the iframe — including
+the channel-list fetch that drives the launcher badge — is captured.
+
+---
+
+## Environment behavior
+
+| Environment | NODE_ENV | Behavior |
+|-------------------|--------------|------------------------------------------|
+| Local dev | `development`| Installed, logs to console |
+| Preview / staging | `development`| Installed, logs to console (override per build) |
+| Production | `production` | No-op — globals untouched, nothing logged |
+
+The gate is `process.env.NODE_ENV === 'production'`. If your CI uses a different
+flag (e.g. `NEXT_PUBLIC_ENV`), update the `isProd` constant at the top of
+`socketLogger.ts`.
+
+---
+
+## What gets logged
+
+Every frame is emitted as a `console.log` under the `[socket]` namespace, with
+auto-parsed JSON when applicable.
+
+| Kind | Source | Direction(s) |
+|------------------|--------------------------------------------|----------------------------|
+| `ws` | `new WebSocket(url)` | `OUT` (send), `IN` (message), `SYSTEM` (open/close/error) |
+| `sse` | `new EventSource(url)` | `IN` (message), `SYSTEM` |
+| `fetch` | `window.fetch(...)` | `OUT` (request), `IN` (response, body ≤ 4 KB) |
+| `xhr` | `XMLHttpRequest#open / send` | `OUT` (request), `IN` (response, body ≤ 4 KB) |
+| `parent-post` | `window.parent.postMessage(...)` | `OUT` (from the iframe) |
+| `parent-receive` | `window` `message` listener | `IN` (incoming postMessage)|
+| `pubnub` | (optional, see below) | `IN`, `OUT`, `SYSTEM` |
+
+Each log entry carries:
+- `ts` — epoch ms timestamp
+- `direction` — `in` / `out` / `system`
+- `kind` — one of the rows above
+- `url` — endpoint or channel
+- `raw` — original payload
+- `parsed` — auto-parsed JSON when `raw` was a JSON string
+
+---
+
+## Inspecting at runtime
+
+Once installed (look for `[socket] Socket logger installed (dev only).` in the
+console), the global is exposed:
+
+```js
+// Last 500 frames (ring buffer)
+window.__socketLogger.history
+
+// Clear console + history
+window.__socketLogger.clear()
+
+// Remove all wrappers and restore native globals
+window.__socketLogger.uninstall()
+```
+
+The history frame shape:
+
+```ts
+type Frame = {
+ ts: number;
+ direction: 'in' | 'out' | 'system';
+ kind: 'ws' | 'sse' | 'xhr' | 'fetch' | 'pubnub' | 'parent-post' | 'parent-receive';
+ url?: string;
+ raw: any;
+ parsed?: any;
+};
+```
+
+---
+
+## PubNub hookup (optional)
+
+If your project loads `pubnub`, attach the same logger to the instance to capture
+PubNub-specific traffic:
+
+```ts
+import PubNub from 'pubnub';
+
+const pubnub = new PubNub({ /* ...config... */ });
+
+pubnub.addListener({
+ message: (m) => console.log('[socket] IN pubnub →', m.channel, m.message),
+ status: (s) => console.log('[socket] SYSTEM pubnub →', s),
+});
+
+const origPublish = pubnub.publish.bind(pubnub);
+pubnub.publish = (args: any) => {
+ console.log('[socket] OUT pubnub →', args.channel, args.message);
+ return origPublish(args);
+};
+```
+
+This complements the wrapper logger by surfacing channel-level traffic
+(PubNub frames aren't carried by `WebSocket` in the global sense).
+
+---
+
+## Why a no-op in production?
+
+- Patches to `window.WebSocket`, `window.fetch`, etc. are global and would leak
+ across all iframes / micro-frontends on the host page.
+- Logging payloads (which can include message bodies, JWTs, customer data) would
+ be a privacy / leak risk.
+- The 4 KB response-body capture is fine for debugging but not desirable in
+ shipped code.
+
+The single guard inside `installSocketLogger()` prevents all of the above:
+
+```ts
+const isProd = typeof process !== 'undefined' && process?.env?.NODE_ENV === 'production';
+// ...
+export function installSocketLogger() {
+ if (typeof window === 'undefined') return;
+ if (isProd) return;
+ if (window.__socketLogger) return;
+ // ... wrappers installed below
+}
+```
+
+---
+
+## Troubleshooting
+
+**Nothing logs.**
+- Confirm you're not on a production build. Check `process.env.NODE_ENV` in the
+ DevTools console: `(await import(/* @vite-ignore */'process')).env.NODE_ENV` or
+ simply run `document.cookie` after build to see the build banner.
+- Confirm the chat widget actually mounted. The install effect runs inside the
+ chat widget component, so it requires `Chatbot` to render.
+
+**`window.__socketLogger` is undefined.**
+- Either `NODE_ENV === 'production'` or the effect never fired.
+
+**Too much noise.**
+- Filter in DevTools with `-/socket/i` to hide, or run
+ `window.__socketLogger.uninstall()` to remove wrappers mid-session.
+
+**`fetch` body shows up as `[object Object]`.**
+- That's normal — non-string bodies (FormData, Blob, ReadableStream) are shown
+ raw. Only string bodies are parsed.
+
+---
+
+## Related files
+
+- `components/Chatbot/utils/socketLogger.ts` — the logger implementation
+- `components/Chatbot/Chatbot.tsx` — calls `installSocketLogger()` once on mount
diff --git a/hooks/HELLO/eventHandlers/embeddingScript/embeddingScriptEventHandler.ts b/hooks/HELLO/eventHandlers/embeddingScript/embeddingScriptEventHandler.ts
index 89721bb7..f210caa3 100644
--- a/hooks/HELLO/eventHandlers/embeddingScript/embeddingScriptEventHandler.ts
+++ b/hooks/HELLO/eventHandlers/embeddingScript/embeddingScriptEventHandler.ts
@@ -1,12 +1,12 @@
import { ThemeContext } from "@/components/AppWrapper";
import { useSendMessageToHello } from "@/components/Chatbot/hooks/useHelloIntegration";
-import { addDomainToHello, saveClientDetails } from "@/config/helloApi";
+import { addDomainToHello, getAllChannels, initializeHelloChat, saveClientDetails } from "@/config/helloApi";
import { CBManger } from "@/hooks/coBrowser/CBManger";
import { EmbeddingScriptEventRegistryInstance } from "@/hooks/CORE/eventHandlers/embeddingScript/embeddingScriptEventHandler";
import { setDataInAppInfoReducer } from "@/store/appInfo/appInfoSlice";
import { setToggleDrawer } from "@/store/chat/chatSlice";
import { setDataInDraftReducer, setVariablesForHelloBot } from "@/store/draftData/draftDataSlice";
-import { setHelloClientInfo, setHelloConfig, setHelloKeysData, setWidgetInfo } from "@/store/hello/helloSlice";
+import { setHelloClientInfo, setHelloConfig, setHelloKeysData, setWidgetInfo, setChannelListData } from "@/store/hello/helloSlice";
import { setDataInInterfaceRedux } from "@/store/interface/interfaceSlice";
import { GetSessionStorageData, SetSessionStorage } from "@/utils/ChatbotUtility";
import { useCustomSelector } from "@/utils/deepCheckSelector";
@@ -176,9 +176,31 @@ const useHandleHelloEmbeddingScriptEvents = (eventHandler: EmbeddingScriptEventR
}
};
- function handleChatbotVisibility(isChatbotOpen = false, id = "") {
+ const isFetchingHelloData = useRef(false);
+
+ async function handleChatbotVisibility(isChatbotOpen = false, id = "") {
dispatch(setDataInAppInfoReducer({ isChatbotOpen }))
dispatch(setDataInDraftReducer({ isChatbotMinimized: false }))
+ if (isChatbotOpen) {
+ try {
+ if (isFetchingHelloData.current) return;
+ isFetchingHelloData.current = true;
+ const [channelsData, widgetInfo] = await Promise.all([
+ getAllChannels(),
+ initializeHelloChat()
+ ]);
+ if (channelsData) {
+ dispatch(setChannelListData(channelsData));
+ }
+ if (widgetInfo) {
+ dispatch(setWidgetInfo(widgetInfo));
+ }
+ } catch (error) {
+ console.error("Failed to fetch chatbot data on open:", error);
+ } finally {
+ isFetchingHelloData.current = false;
+ }
+ }
if (id) {
// Create a mock MessageEvent to pass to handleShowTicket
const mockEvent = {
diff --git a/hooks/socketEventHandler.ts b/hooks/socketEventHandler.ts
index f590f0b6..1fefa251 100644
--- a/hooks/socketEventHandler.ts
+++ b/hooks/socketEventHandler.ts
@@ -47,12 +47,28 @@ export const useSocketEvents = ({
}
const { type } = message || {};
+ // Build a fresh last-message snapshot so the drawer can refresh the
+ // conversation preview + relative time ("23m" → "now") immediately,
+ // without waiting for the next channel-list poll.
+ const buildLastMessage = (msg: any) => {
+ if (!msg) return undefined;
+ const attachment = msg?.content?.attachment;
+ return {
+ timetoken: msg?.timetoken,
+ message_type: msg?.message_type,
+ text: msg?.content?.text,
+ hasAttachment: Array.isArray(attachment) ? attachment.length > 0 : !!attachment,
+ sender_id: msg?.sender_id ?? null,
+ };
+ };
+
// Handle unread count updates
if (message?.new_event && (type === 'chat' || type === 'feedback') && !message?.chat_id) {
const channelId = message?.channel;
dispatch(setUnReadCount({
channelId,
- resetCount: false
+ resetCount: false,
+ lastMessage: buildLastMessage(message)
}));
dispatch(moveChannelToTop({ channelId }));
}
@@ -68,7 +84,8 @@ export const useSocketEvents = ({
addHelloMessage({ ...message, id: messageId }, channel);
dispatch(setUnReadCount({
channelId: channel,
- resetCount: false
+ resetCount: false,
+ lastMessage: buildLastMessage(message)
}));
dispatch(moveChannelToTop({ channelId: channel }));
}
diff --git a/package.json b/package.json
index d894f90e..5c642e85 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev -p 3001 --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
diff --git a/public/chat-widget-local.js b/public/chat-widget-local.js
index 9ea03fb7..7fbf4c69 100644
--- a/public/chat-widget-local.js
+++ b/public/chat-widget-local.js
@@ -166,13 +166,14 @@
const imgElement = document.createElement('div');
imgElement.id = this.elements.chatbotIconImage;
imgElement.innerHTML = `
-
+
`;
chatBotIcon.appendChild(imgElement);
@@ -336,6 +337,11 @@
this.enableDomainTracking();
break;
case 'SET_BADGE_COUNT':
+ try {
+ if (typeof process === 'undefined' || (process && process.env && process.env.NODE_ENV !== 'production')) {
+ console.log('%c[launcher]%c recv SET_BADGE_COUNT', 'color:#7c3aed;font-weight:600', 'color:inherit', data);
+ }
+ } catch (_) { /* ignore */ }
this.updateBadgeCount(data?.badgeCount, data?.channelId || '*');
break;
case 'SHOW_STARTER_QUESTION':
@@ -391,13 +397,30 @@
}
updateBadgeCount(data, channelId) {
+ // [dev-only] trace launcher badge updates in the parent's console
+ if (typeof process === 'undefined' || (process && process.env && process.env.NODE_ENV !== 'production')) {
+ try {
+ if (channelId === '*') {
+ console.log(
+ '%c[launcher]%c SET_BADGE_COUNT',
+ 'color:#7c3aed;font-weight:600',
+ 'color:inherit',
+ { count: data, channelId }
+ );
+ }
+ } catch (_) { /* ignore */ }
+ }
+
const badgeElement = document.getElementById(this.elements.unReadMsgCountBadge);
+ const iconImageElement = document.getElementById(this.elements.chatbotIconImage);
if (badgeElement && channelId === '*') {
if (!data || parseInt(data) === 0) {
badgeElement.style.display = 'none';
+ if (iconImageElement) iconImageElement.classList.remove('has-badge');
} else {
badgeElement.textContent = data;
badgeElement.style.display = 'block'; // or 'block' depending on your layout
+ if (iconImageElement) iconImageElement.classList.add('has-badge');
}
}
const divElement = document.getElementById(`unread-${channelId}`);
@@ -1157,7 +1180,11 @@
}
if (this.state.interfaceLoaded && this.state.delayElapsed) {
const interfaceEmbed = document.getElementById(this.elements.chatbotIconContainer);
- if (!this.hideHelloIcon && (this.helloProps?.hide_launcher !== undefined && (this.helloProps?.hide_launcher === false || this.helloProps?.hide_launcher === 'false')) && !this.helloProps?.isMobileSDK) {
+ // Launcher is visible by default; only hidden when hide_launcher is
+ // explicitly truthy. This keeps the icon shown even if widget-info
+ // fails to load (e.g. API 500) and hide_launcher stays undefined.
+ const launcherHidden = this.helloProps?.hide_launcher === true || this.helloProps?.hide_launcher === 'true';
+ if (!this.hideHelloIcon && !launcherHidden && !this.helloProps?.isMobileSDK) {
if (interfaceEmbed) interfaceEmbed.style.display = 'block';
}
if (this.helloLaunchWidget) this.openChatbot()
diff --git a/public/chat-widget-style.css b/public/chat-widget-style.css
index f401ce79..6025c535 100644
--- a/public/chat-widget-style.css
+++ b/public/chat-widget-style.css
@@ -14,7 +14,6 @@
/* background-color: #3d7bef !important; */
text-align: center;
align-content: center;
- color: white;
font-size: 18px;
/* cursor: pointer; */
z-index: 99999 !important;
@@ -66,7 +65,7 @@
z-index: 2147483647;
display: none;
box-sizing: border-box;
- border-radius: 12px;
+ border-radius: 16px;
overflow: hidden;
border: 1px solid #cecece;
box-shadow: rgba(15, 15, 15, 0.08) 0px 5px 40px 0px;
@@ -80,13 +79,28 @@
[id$="-hello-chatbot-icon-image"] {
background-color: none !important;
object-fit: contain;
- height: 60px !important;
- width: 60px !important;
+ height: 48px !important;
+ width: 48px !important;
margin: 8px 0px 0px 2px !important;
box-sizing: border-box !important;
float: right;
cursor: pointer;
z-index: 999999999 !important;
+ position: relative;
+}
+
+/* Carve a circular notch out of the launcher's top-right corner whenever an
+ unread badge is being shown. The mask matches the launcher's 48x48 inner
+ circle so the badge appears to "punch through" the launcher. */
+[id$="-hello-chatbot-icon-image"].has-badge > div {
+ -webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00Mi45OTkxIDBDNDAuNTcwNyAxLjgyNDQ2IDM5IDQuNzI4OCAzOSA4QzM5IDEzLjE4NTMgNDIuOTQ2NyAxNy40NDg5IDQ4IDE3Ljk1MDZWNDhIMFYwSDQyLjk5OTFaIiBmaWxsPSJjdXJyZW50Q29sb3IiLz4KPC9zdmc+Cg==");
+ mask-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00Mi45OTkxIDBDNDAuNTcwNyAxLjgyNDQ2IDM5IDQuNzI4OCAzOSA4QzM5IDEzLjE4NTMgNDIuOTQ2NyAxNy40NDg5IDQ4IDE3Ljk1MDZWNDhIMFYwSDQyLjk5OTFaIiBmaWxsPSJjdXJyZW50Q29sb3IiLz4KPC9zdmc+Cg==");
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ -webkit-mask-size: 100% 100%;
+ mask-size: 100% 100%;
+ -webkit-mask-position: 0 0;
+ mask-position: 0 0;
}
[id$="-hello-iframe-parent-container"] {
@@ -97,7 +111,7 @@
/* background-color: transparent; */
object-fit: contain;
/* cursor: pointer; */
- z-index: 9999 !important;
+ z-index: 2147483003 !important;
height: auto;
width: auto;
box-sizing: border-box !important;
@@ -107,6 +121,11 @@
right: 18px !important;
}
+#chatbot-logo {
+ display: inherit;
+ place-items: inherit;
+}
+
/* Starter Question Styles */
.hello-starter-question {
z-index: 999999;
@@ -279,8 +298,8 @@
}
.chatbot-icon-interfaceEmbed {
- width: 60px !important;
- height: 60px !important;
+ width: 48px !important;
+ height: 48px !important;
cursor: pointer;
object-fit: contain;
}
@@ -418,12 +437,12 @@
.hello-badge-count {
/* Dimensions & Position */
- min-width: 20px;
- height: 20px;
+ min-width: 18px;
+ height: 18px;
position: absolute;
top: -2px;
- right: 5px;
- padding: 4px;
+ right: -11px;
+ padding: 3px;
box-sizing: border-box;
/* Centering */
diff --git a/public/chatbot-style.css b/public/chatbot-style.css
index 964a92a4..7aeed2ea 100644
--- a/public/chatbot-style.css
+++ b/public/chatbot-style.css
@@ -63,7 +63,7 @@
z-index: 9999;
display: none;
box-sizing: border-box;
- border-radius: 12px;
+ border-radius: 16px;
overflow: hidden;
border: 1px solid #cecece;
}
diff --git a/public/rag.css b/public/rag.css
index 8077266e..6e348e6e 100644
--- a/public/rag.css
+++ b/public/rag.css
@@ -436,7 +436,7 @@
height: 90vh;
max-width: 1200px;
max-height: 800px;
- border-radius: 12px;
+ border-radius: 16px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
overflow: hidden;
diff --git a/store/chat/chatReducerV2.ts b/store/chat/chatReducerV2.ts
index ff030044..fd9e1844 100644
--- a/store/chat/chatReducerV2.ts
+++ b/store/chat/chatReducerV2.ts
@@ -17,6 +17,7 @@ interface ChatState {
// Loading States
loading: boolean;
chatsLoading: boolean;
+ chatsError: boolean;
isFetching: boolean;
// UI States
@@ -59,6 +60,7 @@ export const initialChatState: ChatState = {
// Loading States
loading: false,
chatsLoading: false,
+ chatsError: false,
isFetching: false,
// UI States
@@ -120,6 +122,10 @@ export const chatReducerV2 = {
state.chatsLoading = action.payload;
},
+ setChatsError: (state, action: PayloadAction
) => {
+ state.chatsError = action.payload;
+ },
+
setOptions: (state, action: PayloadAction) => {
state.options = action.payload;
},
diff --git a/store/chat/chatSlice.ts b/store/chat/chatSlice.ts
index 81bef4ac..0ecf1076 100644
--- a/store/chat/chatSlice.ts
+++ b/store/chat/chatSlice.ts
@@ -12,6 +12,7 @@ export const {
removeMessages,
setLoading,
setChatsLoading,
+ setChatsError,
setOptions,
setImages,
clearImages,
diff --git a/store/draftData/draftDataSlice.ts b/store/draftData/draftDataSlice.ts
index 00005ffa..369a791a 100644
--- a/store/draftData/draftDataSlice.ts
+++ b/store/draftData/draftDataSlice.ts
@@ -16,6 +16,7 @@ const draftDataSlice = createSlice({
variables: {} as Record
},
isChatbotMinimized: false as boolean,
+ isChatbotFullScreen: false as boolean,
} as $DraftDataReducerType,
reducers: {
/**
@@ -35,7 +36,8 @@ const draftDataSlice = createSlice({
tabSessionId: "",
widgetToken: "",
chatbotId: "",
- isChatbotMinimized: false
+ isChatbotMinimized: false,
+ isChatbotFullScreen: false
}
},
setVariablesForHelloBot: (state, action: PayloadAction<$DraftDataReducerType>) => {
diff --git a/store/hello/helloReducer.ts b/store/hello/helloReducer.ts
index ead35c38..279c7524 100644
--- a/store/hello/helloReducer.ts
+++ b/store/hello/helloReducer.ts
@@ -71,10 +71,15 @@ export const reducers: ValidateSliceCaseReducers<
setChannelListData(state, action: actionType) {
const chatSessionId = action.urlData?.chatSessionId
if (chatSessionId) {
+ const channels = action.payload?.channels || [];
+ const sortedChannels = [...channels].sort((a: any, b: any) => {
+ if (a.is_closed === b.is_closed) return 0;
+ return a.is_closed ? 1 : -1;
+ });
state[chatSessionId] = {
...state[chatSessionId],
- channelListData: action.payload,
- Channel: action.payload?.channels?.[0]
+ channelListData: { ...action.payload, channels: sortedChannels },
+ Channel: sortedChannels[0]
};
}
},
@@ -141,10 +146,27 @@ export const reducers: ValidateSliceCaseReducers<
}
},
- setUnReadCount(state, action: actionType<{ channelId?: string, resetCount?: boolean }>) {
+ setUnReadCount(state, action: actionType<{
+ channelId?: string,
+ resetCount?: boolean,
+ // Optional last-message snapshot — used by the drawer to refresh
+ // title, preview, and relative time without waiting for the next
+ // channel-list poll.
+ lastMessage?: {
+ timetoken?: number | string,
+ message_type?: string,
+ text?: string,
+ hasAttachment?: boolean,
+ sender_id?: string | null,
+ }
+ }>) {
const chatSessionId = action.urlData?.chatSessionId
if (chatSessionId) {
- const { channelId = state[chatSessionId]?.currentChannelId, resetCount = false } = action.payload;
+ const {
+ channelId = state[chatSessionId]?.currentChannelId,
+ resetCount = false,
+ lastMessage,
+ } = action.payload;
if (!state[chatSessionId]?.channelListData?.channels?.length) return;
@@ -161,7 +183,37 @@ export const reducers: ValidateSliceCaseReducers<
} else {
channel.widget_unread_count = (channel.widget_unread_count || 0) + 1;
}
- emitEventToParent('SET_BADGE_COUNT', { badgeCount: channel.widget_unread_count > 99 ? '99+' : channel.widget_unread_count, channelId: channel?.channel })
+
+ // Refresh last-message preview + timetoken when the caller supplies one.
+ // The drawer's formatRelativeTime() depends on this, and the title /
+ // subtitle fallbacks (message_type → "Voice Call"/"Chat") need fresh
+ // data too.
+ if (lastMessage) {
+ const existing = channel.last_message || {};
+ const existingMsg = existing.message || {};
+ const mergedContent = {
+ ...(existingMsg.content || {}),
+ ...(lastMessage.text !== undefined ? { text: lastMessage.text } : {}),
+ ...(lastMessage.hasAttachment !== undefined ? { attachment: lastMessage.hasAttachment ? [{ }] : null } : {}),
+ };
+ const mergedMsg = {
+ ...existingMsg,
+ ...(lastMessage.message_type ? { message_type: lastMessage.message_type } : {}),
+ ...(lastMessage.timetoken ? { timetoken: lastMessage.timetoken } : {}),
+ ...(lastMessage.sender_id !== undefined ? { sender_id: lastMessage.sender_id } : {}),
+ content: mergedContent,
+ };
+ channel.last_message = {
+ ...existing,
+ timetoken: lastMessage.timetoken ?? existing.timetoken,
+ message: mergedMsg,
+ };
+ }
+
+ emitEventToParent('SET_BADGE_COUNT', {
+ badgeCount: channel.widget_unread_count > 99 ? '99+' : channel.widget_unread_count,
+ channelId: channel?.channel,
+ });
}
},
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 38a3af2a..3631274a 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -15,6 +15,20 @@ export default {
primaryTheme: "rgb(var(--primary-rgb) / )",
secondaryTheme: "rgb(var(--primary-rgb) / )",
},
+ keyframes: {
+ slideUp: {
+ "0%": { transform: "translateY(100%)", opacity: "0" },
+ "100%": { transform: "translateY(0)", opacity: "1" },
+ },
+ fadeIn: {
+ "0%": { opacity: "0" },
+ "100%": { opacity: "1" },
+ },
+ },
+ animation: {
+ slideUp: "slideUp 0.28s cubic-bezier(0.16, 1, 0.3, 1)",
+ fadeIn: "fadeIn 0.2s ease-out",
+ },
},
},
plugins: [
diff --git a/types/reduxCore.ts b/types/reduxCore.ts
index 8e942ca5..e4d8825d 100644
--- a/types/reduxCore.ts
+++ b/types/reduxCore.ts
@@ -19,4 +19,5 @@ export interface $DraftDataReducerType {
variables: Record
}
isChatbotMinimized?: boolean;
-}
\ No newline at end of file
+ isChatbotFullScreen?: boolean;
+}
diff --git a/utils/themeUtility.js b/utils/themeUtility.js
index bee75cdb..ed15ee8f 100644
--- a/utils/themeUtility.js
+++ b/utils/themeUtility.js
@@ -1,18 +1,63 @@
-export function isColorLight(color) {
- // Create an offscreen canvas for measuring the color brightness
+// Mix ratios used for the 3-stop gradient. Adjust here to tweak the look.
+const GRADIENT_STOPS = [0.08, 0.14, 0.20];
+
+// Parse a CSS color into its RGB components [r, g, b].
+// Returns null when called outside a browser (SSR/tests) so callers can fall back.
+function getRgbFromColor(color) {
+ if (typeof document === "undefined") return null;
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
- const context = canvas.getContext("2d");
- context.fillStyle = color;
- context.fillRect(0, 0, 1, 1);
+ const ctx = canvas.getContext("2d");
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
+ return [r, g, b];
+}
- // Get the color data (RGBA) of the filled rectangle
- const [r, g, b] = context.getImageData(0, 0, 1, 1).data;
+// Memoize RGB lookups so repeated calls with the same color skip the canvas work.
+const rgbCache = new Map();
+function getRgbCached(color) {
+ if (rgbCache.has(color)) return rgbCache.get(color);
+ const rgb = getRgbFromColor(color);
+ if (rgb) rgbCache.set(color, rgb);
+ return rgb;
+}
+
+export function isColorLight(color) {
+ const rgb = getRgbCached(color);
+ if (!rgb) return false;
+ const [r, g, b] = rgb;
// Calculate brightness (luminance)
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// Return true if the color is light, otherwise false
return brightness > 128;
-}
\ No newline at end of file
+}
+
+export function getGradientBackground(color) {
+ const rgb = getRgbCached(color);
+ if (!rgb) return color;
+ const [r, g, b] = rgb;
+
+ // Mix white with the base color at each stop for a smooth 3-stop gradient.
+ const mix = (c, ratio) => Math.round(255 + (c - 255) * ratio);
+ const stops = GRADIENT_STOPS.map((ratio) => {
+ const [sr, sg, sb] = [mix(r, ratio), mix(g, ratio), mix(b, ratio)];
+ return `rgb(${sr}, ${sg}, ${sb})`;
+ });
+
+ return `linear-gradient(150deg, ${stops[0]} 0%, ${stops[1]} 50%, ${stops[2]} 100%)`;
+}
+
+// Returns the color as an `rgba(...)` string at the given opacity.
+// Useful for soft tints / badges that follow the theme without hardcoding.
+export function withAlpha(color, alpha) {
+ if (color === null || color === undefined) return color;
+ const rgb = getRgbCached(color);
+ if (!rgb) return color;
+ const [r, g, b] = rgb;
+ const a = Math.max(0, Math.min(1, Number(alpha) || 0));
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+}
diff --git a/utils/utilities.js b/utils/utilities.js
index 60c2c04f..1a229826 100644
--- a/utils/utilities.js
+++ b/utils/utilities.js
@@ -46,8 +46,9 @@ export const generateNewId = (length = 8) => {
};
export const generateChannelId = (companyId = '') => {
+ const numericCompanyId = String(companyId).replace(/\D/g, '');
const uuid = uuidv4().replace(/-/g, '');
- return `ch-comp-${companyId}.${uuid}`;
+ return `ch-comp-${numericCompanyId}.${uuid}`;
};
function getDomain() {