diff --git a/app/globals.css b/app/globals.css index a58bdeec..8967e213 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,12 +4,24 @@ @plugin "daisyui"; @plugin "@tailwindcss/typography"; +/* Hide Scrollbar */ +* { + scrollbar-width: none; +} + +::-webkit-scrollbar { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + display: none; +} + :root { --background: #ffffff; --foreground: #171717; --message-card-background: #f0f0f0; --icon-color: #555555; - --drawer-color: #f2f2f2; + --drawer-color: #ffffff; } [data-theme="dark"] { @@ -88,4 +100,8 @@ html:has(.drawer-toggle:checked) { [data-theme="dark"] .icn { @apply hover:bg-gray-800 hover:text-gray-200 +} + +input, select { + outline: none !important; } \ No newline at end of file diff --git a/components/Chatbot/Chatbot.tsx b/components/Chatbot/Chatbot.tsx index 301b4e3c..40b7487c 100644 --- a/components/Chatbot/Chatbot.tsx +++ b/components/Chatbot/Chatbot.tsx @@ -1,4 +1,5 @@ import { LinearProgress } from '@mui/material'; +import { RefreshCw } from 'lucide-react'; import Image from 'next/image'; import React, { useEffect, useMemo, useRef } from 'react'; @@ -6,6 +7,11 @@ import React, { useEffect, useMemo, useRef } from 'react'; import { MessageContext } from '../Interface-Chatbot/InterfaceChatbot'; import { useReduxStateManagement } from './hooks/useReduxManagement'; import useRtlayerEventManager from './hooks/useRtlayerEventManager'; +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(); // Components import FormComponent from '../FormComponent'; @@ -26,6 +32,7 @@ import { useCustomSelector } from '@/utils/deepCheckSelector'; import { useChatEffects } from './hooks/useChatEffects'; import { useColor } from './hooks/useColor'; import { useHelloEffects } from './hooks/useHelloEffects'; +import { useRetryChats } from './hooks/useHelloIntegration'; import { useReduxEffects } from './hooks/useReduxEffects'; import { useScreenSize } from './hooks/useScreenSize'; @@ -67,7 +74,7 @@ const ActiveChatView = React.memo(() => (
-
+
@@ -80,21 +87,24 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { const messageRef = useRef(null); const timeoutIdRef = useRef(null); - const { backgroundColor } = useColor(); + const { primaryTextColor } = useColor(); const { isSmallScreen } = useScreenSize(); const dispatch = useAppDispatch(); + const retryChats = useRetryChats(); // State management - const { show_widget_form, greetingMessage, isToggledrawer, chatsLoading, messageIds, subThreadId, helloMsgIds } = useCustomSelector((state) => { + const { show_widget_form, greetingMessage, isToggledrawer, chatsLoading, chatsError, messageIds, subThreadId, helloMsgIds, show_msg91 } = useCustomSelector((state) => { const widgetInfo = state.Hello?.[chatSessionId]?.widgetInfo return ({ show_widget_form: typeof widgetInfo?.show_widget_form === 'boolean' ? widgetInfo?.show_widget_form : state.Hello?.[chatSessionId]?.showWidgetForm, greetingMessage: state.Hello?.[chatSessionId]?.greeting as any, isToggledrawer: state.Chat.isToggledrawer, chatsLoading: state.Chat.chatsLoading, + chatsError: state.Chat.chatsError, messageIds: state.Chat.messageIds, subThreadId: state.Chat.subThreadId, - helloMsgIds: state.Chat.helloMsgIds + helloMsgIds: state.Chat.helloMsgIds, + show_msg91: state.Hello?.[chatSessionId]?.widgetInfo?.show_msg91 || false, }) }); @@ -157,13 +167,29 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { {/* Mobile header */} - {/* Loading indicator */} - {chatsLoading && ( + {/* Loading / retry indicator */} + {(chatsLoading || chatsError) && (
- + {chatsLoading && ( + + )} + {chatsError && ( +
+ Couldn't load chat history. + +
+ )}
)} @@ -179,6 +205,39 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { ) : ( )} + + {/* Branding footer */} + {((isHelloUser && show_msg91) || !isHelloUser) && ( +
+
+ {isHelloUser && show_msg91 ? ( + <> + Powered by + + MSG91 + + + ) : ( + <> + Powered by + + GTWY + + + )} +
+
+ )} diff --git a/components/Chatbot/hooks/chatReducer.ts b/components/Chatbot/hooks/chatReducer.ts index 66c0d943..b19c8288 100644 --- a/components/Chatbot/hooks/chatReducer.ts +++ b/components/Chatbot/hooks/chatReducer.ts @@ -15,6 +15,7 @@ export const initialChatState: ChatState = { // Loading States loading: false, chatsLoading: false, + chatsError: false, isFetching: false, // UI States @@ -101,6 +102,11 @@ export const chatReducer = (state: ChatState, action: ChatAction): ChatState => ...state, chatsLoading: action.payload }; + case ChatActionTypes.SET_CHATS_ERROR: + return { + ...state, + chatsError: action.payload + }; case ChatActionTypes.SET_OPTIONS: return { ...state, diff --git a/components/Chatbot/hooks/chatTypes.ts b/components/Chatbot/hooks/chatTypes.ts index e02493e5..22863423 100644 --- a/components/Chatbot/hooks/chatTypes.ts +++ b/components/Chatbot/hooks/chatTypes.ts @@ -26,6 +26,7 @@ export interface ChatState { helloMsgIdAndDataMap: { [subThreadId: string]: { [msgId: string]: any } } loading: boolean; chatsLoading: boolean; + chatsError: boolean; options: any[]; images: string[]; threadId: string; @@ -54,6 +55,7 @@ export interface ReduxSetterActionType { helloMessages?: any[]; loading?: boolean; chatsLoading?: boolean; + chatsError?: boolean; options?: any[]; images?: string[] | Array<{ path: string }>; threadId?: string; @@ -78,6 +80,7 @@ export enum ChatActionTypes { UPDATE_LAST_ASSISTANT_MESSAGE = 'UPDATE_LAST_ASSISTANT_MESSAGE', SET_LOADING = 'SET_LOADING', SET_CHATS_LOADING = 'SET_CHATS_LOADING', + SET_CHATS_ERROR = 'SET_CHATS_ERROR', SET_OPTIONS = 'SET_OPTIONS', SET_IMAGES = 'SET_IMAGES', CLEAR_IMAGES = 'CLEAR_IMAGES', @@ -112,6 +115,7 @@ export type ChatAction = | { type: ChatActionTypes.UPDATE_LAST_ASSISTANT_MESSAGE; payload: Partial } | { type: ChatActionTypes.SET_LOADING; payload: boolean } | { type: ChatActionTypes.SET_CHATS_LOADING; payload: boolean } + | { type: ChatActionTypes.SET_CHATS_ERROR; payload: boolean } | { type: ChatActionTypes.SET_OPTIONS; payload: any[] } | { type: ChatActionTypes.SET_IMAGES; payload: string[] } | { type: ChatActionTypes.CLEAR_IMAGES } diff --git a/components/Chatbot/hooks/useChatActions.ts b/components/Chatbot/hooks/useChatActions.ts index 6003f940..69c91a38 100644 --- a/components/Chatbot/hooks/useChatActions.ts +++ b/components/Chatbot/hooks/useChatActions.ts @@ -2,7 +2,7 @@ import { ChatContext } from '@/components/Chatbot-Wrapper/ChatbotWrapper'; import { errorToast } from '@/components/customToast'; import { MessageContext } from '@/components/Interface-Chatbot/InterfaceChatbot'; import { getAllThreadsApi, getPreviousMessage, sendDataToAction, sendFeedbackAction } from '@/config/api'; -import { removeMessages, setChatsLoading, setData, setHelloEventMessage, setImages, setInitialMessages, setIsFetching, setLoading, setNewMessage, setOptions, setPaginateMessages, setStarterQuestions, setToggleDrawer, updateLastAssistantMessage, updateSingleMessage } from '@/store/chat/chatSlice'; +import { removeMessages, setChatsError, setChatsLoading, setData, setHelloEventMessage, setImages, setInitialMessages, setIsFetching, setLoading, setNewMessage, setOptions, setPaginateMessages, setStarterQuestions, setToggleDrawer, updateLastAssistantMessage, updateSingleMessage } from '@/store/chat/chatSlice'; import { setThreads } from '@/store/interface/interfaceSlice'; import { useCustomSelector } from '@/utils/deepCheckSelector'; import { PAGE_SIZE } from '@/utils/enums'; @@ -258,6 +258,7 @@ export const useChatActions = () => { setToggleDrawer: (payload: boolean) => globalDispatch(setToggleDrawer(payload)), setLoading: (payload: boolean) => globalDispatch(setLoading(payload)), setChatsLoading: (payload: boolean) => globalDispatch(setChatsLoading(payload)), + setChatsError: (payload: boolean) => globalDispatch(setChatsError(payload)), setImages: (payload: string[]) => globalDispatch(setImages(payload)), setOptions: (payload: string[]) => globalDispatch(setOptions(payload)), setNewMessage: (payload: boolean) => globalDispatch(setNewMessage(payload)), diff --git a/components/Chatbot/hooks/useColor.ts b/components/Chatbot/hooks/useColor.ts index 9b7ad0ce..b98b5b78 100644 --- a/components/Chatbot/hooks/useColor.ts +++ b/components/Chatbot/hooks/useColor.ts @@ -1,10 +1,22 @@ -import { isColorLight } from "@/utils/themeUtility"; +import { getGradientBackground, isColorLight, withAlpha } from "@/utils/themeUtility"; import { useTheme } from "@mui/material"; export const useColor = () => { const theme = useTheme(); - const backgroundColor = theme.palette.primary.main; - const textColor = isColorLight(backgroundColor) ? "black" : "white"; + const primaryColor = theme.palette.primary.main; + const isLight = isColorLight(primaryColor); + const textColor = isLight ? "black" : "white"; - return { backgroundColor, textColor } + return { + primaryBgColor: primaryColor, + // Kept separate from primaryBgColor so the text color can be customized independently in the future. + primaryTextColor: primaryColor, + foregroundColor: textColor, + primaryGradientBg: getGradientBackground(primaryColor), + primaryTintColor: withAlpha(primaryColor, 0.12), + primaryHoverTintColor: withAlpha(primaryColor, 0.08), + headerHoverBg: isLight + ? "rgba(0, 0, 0, 0.08)" + : "rgba(255, 255, 255, 0.18)", + } } \ No newline at end of file diff --git a/components/Chatbot/hooks/useHelloIntegration.ts b/components/Chatbot/hooks/useHelloIntegration.ts index 6f5dd6cc..da364e9b 100644 --- a/components/Chatbot/hooks/useHelloIntegration.ts +++ b/components/Chatbot/hooks/useHelloIntegration.ts @@ -58,19 +58,25 @@ export const useHelloMessages = () => { export const useFetchHelloPreviousHistory = () => { const { chatSessionId } = useHelloContext(); const globalDispatch = useAppDispatch(); - const { setChatsLoading } = useChatActions(); + const { setChatsLoading, setChatsError } = useChatActions(); const { setHelloMessages } = useHelloMessages(); - const { uuid, currentChannelId } = useReduxStateManagement({ + const { uuid, currentChannelId, companyId } = useReduxStateManagement({ chatSessionId, tabSessionId: useHelloContext().tabSessionId }); return useCallback((dynamicChannelId?: string) => { - const channelId = dynamicChannelId || currentChannelId; + // Backend regex: ^ch-comp-(\d+)\.([0-9a-f]{32})$ — must be 32 hex chars. + // Some code paths persist a 24-char ObjectId (k_clientId/a_clientId), so + // regenerate whenever the stored id does not match. + const storedChannel = (dynamicChannelId || currentChannelId) || ''; + const isValid = /^[0-9a-f]{32}$/.test(String(storedChannel).split('.')[1] || ''); + const channelId = isValid ? storedChannel : generateChannelId(companyId); if (!channelId || !uuid) return; setChatsLoading(true); + setChatsError(false); getHelloChatHistoryApi(channelId) .then((response) => { const helloChats = response?.data?.data; @@ -88,20 +94,21 @@ export const useFetchHelloPreviousHistory = () => { }) .catch((error) => { console.error("Error fetching Hello chat history:", error); + setChatsError(true); }) .finally(() => { setChatsLoading(false); }); - }, [currentChannelId, uuid, setChatsLoading, setHelloMessages, globalDispatch]); + }, [currentChannelId, uuid, companyId, setChatsLoading, setChatsError, setHelloMessages, globalDispatch]); }; export const useGetMoreHelloChats = () => { const { chatSessionId } = useHelloContext(); const globalDispatch = useAppDispatch(); - const { setChatsLoading } = useChatActions(); + const { setChatsLoading, setChatsError } = useChatActions(); const { addHelloMessage } = useHelloMessages(); - const { uuid, currentChannelId } = useReduxStateManagement({ + const { uuid, currentChannelId, companyId } = useReduxStateManagement({ chatSessionId, tabSessionId: useHelloContext().tabSessionId }); @@ -112,10 +119,15 @@ export const useGetMoreHelloChats = () => { })); return useCallback(() => { - if (!currentChannelId || !uuid || !hasMoreMessages) return; + // Backend regex: ^ch-comp-(\d+)\.([0-9a-f]{32})$ — must be 32 hex chars. + const storedChannel = currentChannelId || ''; + const isValid = /^[0-9a-f]{32}$/.test(String(storedChannel).split('.')[1] || ''); + const channelId = isValid ? storedChannel : generateChannelId(companyId); + if (!channelId || !uuid || !hasMoreMessages) return; setChatsLoading(true); - getHelloChatHistoryApi(currentChannelId, skip) + setChatsError(false); + getHelloChatHistoryApi(channelId, skip) .then((response) => { const helloChats = response?.data?.data; if (Array.isArray(helloChats) && helloChats.length > 0) { @@ -132,11 +144,25 @@ export const useGetMoreHelloChats = () => { }) .catch((error) => { console.error("Error fetching more Hello chat history:", error); + setChatsError(true); }) .finally(() => { setChatsLoading(false); }); - }, [currentChannelId, uuid, setChatsLoading, addHelloMessage, hasMoreMessages, skip, globalDispatch]); + }, [currentChannelId, uuid, companyId, setChatsLoading, setChatsError, addHelloMessage, hasMoreMessages, skip, globalDispatch]); +}; + +/** + * Combined "retry the chat history fetch" hook — re-runs the initial + * `getHelloChatHistoryApi` call. Used by the inline Retry pill in the header. + */ +export const useRetryChats = () => { + const fetchHelloPreviousHistory = useFetchHelloPreviousHistory(); + const fetchMoreHelloChats = useGetMoreHelloChats(); + return useCallback(() => { + fetchHelloPreviousHistory(); + fetchMoreHelloChats(); + }, [fetchHelloPreviousHistory, fetchMoreHelloChats]); }; export const useFetchChannels = () => { @@ -224,13 +250,20 @@ export const useOnSendHello = () => { try { - const channelIdToUse = newChannelId || currentChannelId || overrideChannelId; - const chatIdToUse = overrideChatId || currentChatId; - const teamIdToUse = overrideTeamId || currentTeamId; + const channelIdToUse = newChannelId !== undefined ? newChannelId : (currentChannelId || overrideChannelId); + const chatIdToUse = overrideChatId !== undefined ? overrideChatId : currentChatId; + const teamIdToUse = overrideTeamId !== undefined ? overrideTeamId : currentTeamId; - let workingChannelId = channelIdToUse; - if (!chatIdToUse && !channelIdToUse) { - workingChannelId = generateChannelId(companyId); + // Backend regex: ^ch-comp-(\d+)\.([0-9a-f]{32})$ — must be 32 hex chars. + // Some code paths persist a 24-char ObjectId (k_clientId/a_clientId), so + // regenerate whenever the chosen channel id does not match. + const isChannelHexValid = (id: string) => + /^[0-9a-f]{32}$/.test(String(id || '').split('.')[1] || ''); + + let workingChannelId = isChannelHexValid(channelIdToUse) + ? channelIdToUse + : generateChannelId(companyId); + if (!chatIdToUse && (!channelIdToUse || !isChannelHexValid(channelIdToUse))) { dispatch(setDataInAppInfoReducer({ subThreadId: workingChannelId })); @@ -303,10 +336,24 @@ export const useOnSendHello = () => { } const data = await sendMessageToHelloApi(message, attachments, channelDetail, chatIdToUse, helloVariables, voiceCall, demo_widget, widget_msg_id, repliedOn); if (data && (!chatIdToUse || !channelIdToUse || demo_widget)) { + // Prefer the locally-generated 32-char channel id (matches backend regex + // ^ch-comp-(\d+)\.([0-9a-f]{32})$) over the backend's echoed channel, + // which may contain a 24-char ObjectId suffix and would be rejected by + // /get-history/. Fall back to the backend's echo only if it already has + // a 32-char hex suffix. + const backendChannel = data?.['channel']; + const channelToPersist = (() => { + if (workingChannelId) return workingChannelId; + if (backendChannel) { + const suffix = String(backendChannel).split('.')[1]; + if (suffix && /^[0-9a-f]{32}$/.test(suffix)) return backendChannel; + } + return generateChannelId(companyId); + })(); dispatch(setDataInAppInfoReducer({ - subThreadId: data?.['channel'], + subThreadId: channelToPersist, currentChatId: data?.['id'], - currentChannelId: data?.['channel'], + currentChannelId: channelToPersist, overrideChannelId: "" })); // no need to append user message again this time diff --git a/components/Chatbot/hooks/useReduxManagement.ts b/components/Chatbot/hooks/useReduxManagement.ts index 0eb50c7b..f2e4b114 100644 --- a/components/Chatbot/hooks/useReduxManagement.ts +++ b/components/Chatbot/hooks/useReduxManagement.ts @@ -33,26 +33,52 @@ export const useReduxStateManagement = ({ currentChannelId, currentTeamId, isDefaultNavigateToChatScreen, - overrideChannelId - } = useCustomSelector((state) => ({ - interfaceContextData: state.Interface?.[chatSessionId]?.interfaceContext?.variables, - isHelloUser: state.draftData?.isHelloUser || false, - uuid: state.Hello?.[chatSessionId]?.channelListData?.uuid, - unique_id: state.Hello?.[chatSessionId]?.channelListData?.unique_id, - presence_channel: state.Hello?.[chatSessionId]?.channelListData?.presence_channel, - team_id: state.Hello?.[chatSessionId]?.widgetInfo?.team?.[0]?.id, - isDefaultNavigateToChatScreen: isDefaultNavigateToChatScreenFn(state, chatSessionId), - chat_id: state.Hello?.[chatSessionId]?.Channel?.id, - channelId: state.Hello?.[chatSessionId]?.Channel?.channel || null, - mode: state.Hello?.[chatSessionId]?.mode || [], - selectedAiServiceAndModal: state.Interface?.[chatSessionId]?.selectedAiServiceAndModal || null, - unique_id_hello: state?.Hello?.[chatSessionId]?.helloConfig?.unique_id, - widgetToken: state?.Hello?.[chatSessionId]?.helloConfig?.widgetToken, - currentChatId: state?.appInfo?.[tabSessionId]?.currentChatId, - currentChannelId: state?.appInfo?.[tabSessionId]?.currentChannelId, - currentTeamId: state?.appInfo?.[tabSessionId]?.currentTeamId, - overrideChannelId: state?.appInfo?.[tabSessionId]?.overrideChannelId, - })); + overrideChannelId, + companyId + } = useCustomSelector((state) => { + const channels = state.Hello?.[chatSessionId]?.channelListData?.channels || []; + const fallbackChat = (() => { + if (!channels?.length) return null; + const openChats = channels + .filter((ch: any) => !ch.is_closed) + .sort((a: any, b: any) => (b.last_message?.timetoken || 0) - (a.last_message?.timetoken || 0)); + return openChats[0] || channels[0]; + })(); + + // Treat "" / null / undefined as a deliberate "start fresh" signal, + // not as a fallback trigger. Only fall back when explicitly cleared. + const isExplicitlyEmpty = (v: any) => v === '' || v === null || v === undefined; + const appInfoChannelId = state?.appInfo?.[tabSessionId]?.currentChannelId; + const appInfoChatId = state?.appInfo?.[tabSessionId]?.currentChatId; + const appInfoTeamId = state?.appInfo?.[tabSessionId]?.currentTeamId; + + return { + interfaceContextData: state.Interface?.[chatSessionId]?.interfaceContext?.variables, + isHelloUser: state.draftData?.isHelloUser || false, + uuid: state.Hello?.[chatSessionId]?.channelListData?.uuid, + unique_id: state.Hello?.[chatSessionId]?.channelListData?.unique_id, + presence_channel: state.Hello?.[chatSessionId]?.channelListData?.presence_channel, + team_id: state.Hello?.[chatSessionId]?.widgetInfo?.team?.[0]?.id, + isDefaultNavigateToChatScreen: isDefaultNavigateToChatScreenFn(state, chatSessionId), + chat_id: state.Hello?.[chatSessionId]?.Channel?.id, + channelId: state.Hello?.[chatSessionId]?.Channel?.channel || null, + mode: state.Hello?.[chatSessionId]?.mode || [], + selectedAiServiceAndModal: state.Interface?.[chatSessionId]?.selectedAiServiceAndModal || null, + unique_id_hello: state?.Hello?.[chatSessionId]?.helloConfig?.unique_id, + widgetToken: state?.Hello?.[chatSessionId]?.helloConfig?.widgetToken, + currentChannelId: isExplicitlyEmpty(appInfoChannelId) + ? '' + : (appInfoChannelId ?? fallbackChat?.channel ?? ''), + currentChatId: isExplicitlyEmpty(appInfoChatId) + ? '' + : (appInfoChatId ?? fallbackChat?.id ?? ''), + currentTeamId: isExplicitlyEmpty(appInfoTeamId) + ? '' + : (appInfoTeamId ?? fallbackChat?.team_id ?? ''), + overrideChannelId: state?.appInfo?.[tabSessionId]?.overrideChannelId, + companyId: state.Hello?.[chatSessionId]?.widgetInfo?.company_id || '', + }; + }); return { interfaceContextData, @@ -71,6 +97,7 @@ export const useReduxStateManagement = ({ currentChatId, currentChannelId, currentTeamId, - overrideChannelId + overrideChannelId, + companyId }; }; \ No newline at end of file diff --git a/components/Chatbot/utils/socketLogger.ts b/components/Chatbot/utils/socketLogger.ts new file mode 100644 index 00000000..21038390 --- /dev/null +++ b/components/Chatbot/utils/socketLogger.ts @@ -0,0 +1,242 @@ +/** + * Runtime logger for socket / realtime traffic. + * Only active when NODE_ENV !== 'production'. + * Safe to import & call in any environment — installs nothing when disabled. + */ + +type Frame = { + ts: number; + direction: 'in' | 'out' | 'system'; + kind: 'ws' | 'sse' | 'xhr' | 'fetch' | 'pubnub' | 'parent-post' | 'parent-receive'; + url?: string; + raw: any; + parsed?: any; +}; + +declare global { + interface Window { + __socketLogger?: { + history: Frame[]; + clear: () => void; + uninstall: () => void; + }; + } +} + +const NS = '%c[socket]'; +const NS_STYLE = 'color:#2563eb;font-weight:600;'; + +const isProd = + typeof process !== 'undefined' && process?.env?.NODE_ENV === 'production'; + +function safeParse(s: any): any { + if (typeof s !== 'string') return undefined; + try { return JSON.parse(s); } catch { return s; } +} + +function push(frame: Frame) { + if (!window.__socketLogger) return; + window.__socketLogger.history.push(frame); + if (window.__socketLogger.history.length > 500) { + window.__socketLogger.history.shift(); + } +} + +function log(frame: Frame) { + push(frame); + const tag = `${NS} ${frame.direction.toUpperCase()} ${frame.kind} ${frame.url ? '→ ' + frame.url : ''}`; + if (frame.parsed !== undefined) { + console.log(tag, NS_STYLE, frame.parsed); + } else { + console.log(tag, NS_STYLE, frame.raw); + } +} + +export function installSocketLogger() { + if (typeof window === 'undefined') return; + if (isProd) return; // disabled in production + if (window.__socketLogger) return; // already installed + + // Keep references for full uninstall (useful in tests / HMR). + const OrigWS = window.WebSocket; + const OrigES = window.EventSource; + const OrigFetch = window.fetch; + const OrigXHR = window.XMLHttpRequest; + + window.__socketLogger = { + history: [], + clear: () => { + window.__socketLogger!.history = []; + console.clear(); + }, + uninstall: () => { + // @ts-ignore + window.WebSocket = OrigWS; + // @ts-ignore + window.EventSource = OrigES; + window.fetch = OrigFetch; + // @ts-ignore + window.XMLHttpRequest = OrigXHR; + delete window.__socketLogger; + }, + }; + + // ---------- WebSocket ---------- + class WrappedWS extends OrigWS { + constructor(url: string | URL, protocols?: string | string[]) { + super(url, protocols as any); + const urlStr = String(url); + log({ ts: Date.now(), direction: 'system', kind: 'ws', url: urlStr, raw: 'OPEN' }); + this.addEventListener('message', (e: MessageEvent) => { + log({ + ts: Date.now(), direction: 'in', kind: 'ws', url: urlStr, + raw: e.data, parsed: safeParse(e.data), + }); + }); + const origSend = this.send.bind(this); + this.send = (data: any) => { + log({ + ts: Date.now(), direction: 'out', kind: 'ws', url: urlStr, + raw: data, parsed: safeParse(data), + }); + return origSend(data); + }; + this.addEventListener('close', (e) => { + log({ ts: Date.now(), direction: 'system', kind: 'ws', url: urlStr, raw: `CLOSE ${e.code}` }); + }); + this.addEventListener('error', (e) => { + log({ ts: Date.now(), direction: 'system', kind: 'ws', url: urlStr, raw: e }); + }); + } + } + // @ts-ignore + window.WebSocket = WrappedWS; + + // ---------- EventSource (SSE) ---------- + if (OrigES) { + class WrappedES extends OrigES { + constructor(url: string, conf?: EventSourceInit) { + super(url, conf); + const urlStr = String(url); + log({ ts: Date.now(), direction: 'system', kind: 'sse', url: urlStr, raw: 'OPEN' }); + ['message', 'error', 'open'].forEach((evt) => { + this.addEventListener(evt, (e: any) => { + log({ + ts: Date.now(), + direction: evt === 'message' ? 'in' : 'system', + kind: 'sse', url: urlStr, + raw: e?.data ?? e?.type, parsed: safeParse(e?.data), + }); + }); + }); + } + } + // @ts-ignore + window.EventSource = WrappedES; + } + + // ---------- fetch ---------- + window.fetch = async (...args: any[]) => { + const [input, init] = args; + const url = typeof input === 'string' ? input : (input as Request).url; + log({ ts: Date.now(), direction: 'out', kind: 'fetch', url, raw: { method: (init?.method || 'GET'), body: init?.body } }); + const res = await OrigFetch(...args); + const clone = res.clone(); + clone.text().then((t) => { + log({ + ts: Date.now(), direction: 'in', kind: 'fetch', url, + raw: { status: res.status, body: t.slice(0, 4000) }, + parsed: safeParse(t), + }); + }).catch(() => {}); + return res; + }; + + // ---------- XMLHttpRequest ---------- + class WrappedXHR extends OrigXHR { + private _url = ''; + private _method = ''; + open(method: string, url: string | URL, ...rest: any[]) { + this._method = method; + this._url = String(url); + return super.open(method, url as any, ...rest); + } + send(body?: any) { + log({ ts: Date.now(), direction: 'out', kind: 'xhr', url: this._url, raw: { method: this._method, body } }); + this.addEventListener('load', () => { + log({ + ts: Date.now(), direction: 'in', kind: 'xhr', url: this._url, + raw: { status: this.status, body: (this.responseText || '').slice(0, 4000) }, + parsed: safeParse(this.responseText), + }); + }); + return super.send(body); + } + } + // @ts-ignore + window.XMLHttpRequest = WrappedXHR; + + // ---------- axios (if loaded) ---------- + // axios uses XHR under the hood, but if a project swaps in a custom adapter + // (e.g. http adapter in Node tests), we still want a hook. We patch lazily on + // first axios import and re-apply wrappers. + const tryPatchAxios = () => { + const ax = (window as any).axios; + if (!ax || (ax as any).__socketLoggerPatched) return; + try { + ax.interceptors.request.use((req: any) => { + log({ + ts: Date.now(), direction: 'out', kind: 'fetch', + url: req?.url, raw: { method: req?.method, data: req?.data }, + }); + return req; + }); + ax.interceptors.response.use( + (res: any) => { + log({ + ts: Date.now(), direction: 'in', kind: 'fetch', + url: res?.config?.url, + raw: { status: res?.status, data: typeof res?.data === 'string' ? res.data.slice(0, 4000) : '' }, + parsed: typeof res?.data === 'string' ? safeParse(res.data) : undefined, + }); + return res; + }, + (err: any) => { + log({ + ts: Date.now(), direction: 'in', kind: 'fetch', + url: err?.config?.url, raw: { status: err?.response?.status, error: err?.message }, + }); + return Promise.reject(err); + }, + ); + (ax as any).__socketLoggerPatched = true; + } catch { /* axios missing — ignore */ } + }; + tryPatchAxios(); + // Re-attempt in case axios is loaded async. + setTimeout(tryPatchAxios, 0); + setTimeout(tryPatchAxios, 1000); + + // ---------- parent ↔ iframe postMessage ---------- + try { + const origPost = window.parent.postMessage.bind(window.parent); + (window.parent as any).postMessage = function (msg: any, targetOrigin: string, ...rest: any[]) { + log({ ts: Date.now(), direction: 'out', kind: 'parent-post', raw: msg, parsed: safeParse(msg) }); + return origPost(msg, targetOrigin, ...rest); + }; + } catch { /* cross-origin parent */ } + + window.addEventListener('message', (e: MessageEvent) => { + log({ + ts: Date.now(), direction: 'in', kind: 'parent-receive', + raw: e.data, parsed: safeParse(e.data), + }); + }); + + console.log( + '%c[socket]%c Socket logger installed (dev only) — watching ws / sse / fetch / xhr / postMessage. window.__socketLogger.history / .clear() / .uninstall()', + NS_STYLE, 'color:inherit' + ); + // Heartbeat — proves the iframe is alive in case pages spam the console. + console.log('[socket] ready @', new Date().toISOString()); +} diff --git a/components/FormComponent.tsx b/components/FormComponent.tsx index 98713c5d..2ce3732d 100644 --- a/components/FormComponent.tsx +++ b/components/FormComponent.tsx @@ -6,7 +6,7 @@ import { setHelloClientInfo, setHelloKeysData } from "@/store/hello/helloSlice"; import { GetSessionStorageData } from "@/utils/ChatbotUtility"; import { useCustomSelector } from "@/utils/deepCheckSelector"; import { splitNumber } from "@/utils/utilities"; -import { BookText, Loader2, Mail, Phone, Send, User } from "lucide-react"; +import { Loader2, Mail, MessageSquare, Phone, Send, User } from "lucide-react"; import React, { useEffect, useState } from "react"; import { useDispatch } from "react-redux"; import { useColor } from "./Chatbot/hooks/useColor"; @@ -36,13 +36,17 @@ interface FormErrors { } function FormComponent({ chatSessionId }: FormComponentProps) { - const { textColor, backgroundColor } = useColor(); + const { foregroundColor, primaryTextColor, primaryBgColor } = useColor(); const dispatch = useDispatch(); - const { showWidgetForm, open, userData } = useCustomSelector((state) => ({ - showWidgetForm: state.Hello?.[chatSessionId]?.showWidgetForm ?? true, - open: state.Chat.openHelloForm, - userData: state.Hello?.[chatSessionId]?.clientInfo - })); + const { showWidgetForm, open, userData, isFullScreen } = useCustomSelector((state) => { + const fullScreen = state.Hello?.[chatSessionId]?.helloConfig?.fullScreen; + return { + showWidgetForm: state.Hello?.[chatSessionId]?.showWidgetForm ?? true, + open: state.Chat.openHelloForm, + userData: state.Hello?.[chatSessionId]?.clientInfo, + isFullScreen: (fullScreen === true || fullScreen === 'true') ?? false + }; + }); const scriptParams = JSON.parse(GetSessionStorageData('helloConfig') || '{}') const { isSmallScreen } = useScreenSize(); const [formData, setFormData] = useState({ @@ -137,32 +141,39 @@ function FormComponent({ chatSessionId }: FormComponentProps) { if (!open && !showWidgetForm) return null; if (!open && showWidgetForm) return ( -
setOpen(true)} - style={{ - background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, - color: textColor - }} - > -
-
- +
+
setOpen(true)} + className="flex items-center gap-3 rounded-xl px-3 py-2.5 cursor-pointer transition-all hover:opacity-95 hover:shadow-md" + style={{ backgroundColor: primaryBgColor, color: foregroundColor }} + > +
+
-
- Enter your details -

Click here to provide your information

+
+
Enter your details
+
Click here to provide your information
); return ( -
-
- {/* Card header */} - < div className="bg-primary text-white p-5 rounded-t-lg" style={{ - background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, - color: textColor +
setOpen(false)} + > +
e.stopPropagation()} + > + {/* Card header (sticky); drag handle shown only in bottom-sheet mode */} + < div className={`bg-primary text-white px-5 py-4 rounded-t-2xl sticky top-0 z-10`} style={{ + background: `linear-gradient(to right, ${primaryBgColor}, ${primaryBgColor}CC)`, + color: foregroundColor }}>

Enter your details

@@ -171,11 +182,15 @@ function FormComponent({ chatSessionId }: FormComponentProps) {

{/* Form content */} -
+ {/* Name field */}
-