From 3bd3bead6462f5c1980a6b4a1769d818bdbb9267 Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Sat, 20 Jun 2026 12:32:52 +0530 Subject: [PATCH 01/11] chore: add port 3001 configuration to package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From f299f371d7651ed9d4a594105d28fffe9d167cdf Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Sat, 20 Jun 2026 17:26:27 +0530 Subject: [PATCH 02/11] refactor: add QuickActionMenu and fix z-index issues --- components/FormComponent.tsx | 2 +- .../Interface-Chatbot/ChatbotDrawer.tsx | 100 ++++++++--- .../Interface-Chatbot/ChatbotHeader.tsx | 70 +++++++- .../Interface-Chatbot/InterfaceChatbot.css | 2 +- .../Interface-Chatbot/QuickActionsMenu.tsx | 169 ++++++++++++++++++ 5 files changed, 311 insertions(+), 32 deletions(-) create mode 100644 components/Interface-Chatbot/QuickActionsMenu.tsx diff --git a/components/FormComponent.tsx b/components/FormComponent.tsx index 98713c5d..c20c884e 100644 --- a/components/FormComponent.tsx +++ b/components/FormComponent.tsx @@ -138,7 +138,7 @@ function FormComponent({ chatSessionId }: FormComponentProps) { if (!open && !showWidgetForm) return null; if (!open && showWidgetForm) return (
setOpen(true)} style={{ background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index 1a036ae8..15eda6b4 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -1,8 +1,8 @@ 'use client'; import { lighten } from "@mui/material"; -import { AlignLeft, ChevronRight, SquarePen, Users, X } from "lucide-react"; -import { useContext, useEffect, useMemo } from "react"; +import { AlignLeft, ChevronRight, SquarePen, Users } from "lucide-react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; // API and Services @@ -16,6 +16,7 @@ import { useReduxStateManagement } from "../Chatbot/hooks/useReduxManagement"; // Redux Actions import { setDataInAppInfoReducer } from "@/store/appInfo/appInfoSlice"; +import { setDataInDraftReducer } from "@/store/draftData/draftDataSlice"; import { setThreads } from "@/store/interface/interfaceSlice"; // Utils and Types @@ -25,6 +26,8 @@ import { useColor } from "../Chatbot/hooks/useColor"; import { useScreenSize } from "../Chatbot/hooks/useScreenSize"; import { MessageContext } from "./InterfaceChatbot"; import { useOnSendHello } from "../Chatbot/hooks/useHelloIntegration"; +import { emitEventToParent } from "@/utils/emitEventsToParent/emitEventsToParent"; +import QuickActionsMenu from "./QuickActionsMenu"; const createRandomId = () => Math.random().toString(36).substring(2, 15); @@ -75,9 +78,13 @@ const ChatbotDrawer = ({ tagline, hideCloseButton, voice_call_widget, - show_msg91 + show_msg91, + isChatbotMinimized, + isMobileSDK, + isFullScreen } = useCustomSelector((state) => { const show_close_button = state.Hello?.[chatSessionId]?.helloConfig?.show_close_button + const helloFullScreen = state.Hello?.[chatSessionId]?.helloConfig?.fullScreen return { subThreadList: state.Interface?.[chatSessionId]?.interfaceContext?.[bridgeName]?.threadList?.[threadId] || [], teamsList: state.Hello?.[chatSessionId]?.widgetInfo?.teams || [], @@ -87,7 +94,10 @@ const ChatbotDrawer = ({ tagline: state.Hello?.[chatSessionId]?.widgetInfo?.tagline || '', hideCloseButton: typeof show_close_button === 'boolean' ? !show_close_button : state.appInfo?.[tabSessionId]?.hideCloseButton || false, voice_call_widget: state.Hello?.[chatSessionId]?.widgetInfo?.voice_call_widget || false, - show_msg91: state.Hello?.[chatSessionId]?.widgetInfo?.show_msg91 || false + show_msg91: state.Hello?.[chatSessionId]?.widgetInfo?.show_msg91 || false, + isChatbotMinimized: state.draftData?.isChatbotMinimized || false, + isMobileSDK: state.Hello?.[chatSessionId]?.helloConfig?.isMobileSDK || false, + isFullScreen: (helloFullScreen === true || helloFullScreen === 'true') ?? false }; }); @@ -401,18 +411,70 @@ const ChatbotDrawer = ({ window.parent.postMessage({ type: "CLOSE_CHATBOT" }, "*"); }; - const CloseButton = useMemo(() => { - if (hideCloseButton === true || hideCloseButton === "true" || !isSmallScreen) return null; + const handleMinimizeChatbot = (value: boolean) => { + dispatch(setDataInDraftReducer({ isChatbotMinimized: value })); + }; + + const [fullScreen, setFullScreen] = useState(false); + + const toggleFullScreen = (enter: boolean) => { + if (!window?.parent) return; + setFullScreen(enter); + const message = enter + ? { type: "ENTER_FULL_SCREEN_CHATBOT" } + : { type: "EXIT_FULL_SCREEN_CHATBOT" }; + window.parent.postMessage(message, "*"); + }; + + const handleToggleMinimize = () => { + if (!isChatbotMinimized && fullScreen) { + toggleFullScreen(false); + } + handleMinimizeChatbot(!isChatbotMinimized); + if (!isChatbotMinimized) { + emitEventToParent('MINIMIZE_CHATBOT'); + } else { + toggleFullScreen(false); + } + }; + + // Quick Actions dropdown for the drawer header + const canMinimize = isHelloUser && !isMobileSDK; + const canFullScreen = !isMobileSDK && !isFullScreen; + + const DrawerQuickActionsMenu = useMemo(() => { + if (!isToggledrawer) return null; return ( -
- -
+ toggleFullScreen(!fullScreen)} + onNewConversation={handleCreateNewSubThread} + onClose={() => handleCloseChatbot()} + triggerClassName="p-2 hover:bg-gray-200 rounded-full transition-colors icn" + menuClassName="absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-[9999] py-1" + useIconColor + /> ); - }, [hideCloseButton, handleCloseChatbot]); + }, [ + isToggledrawer, + isHelloUser, + isChatbotMinimized, + fullScreen, + canMinimize, + canFullScreen, + hideCloseButton, + handleToggleMinimize, + toggleFullScreen, + handleCreateNewSubThread, + handleCloseChatbot + ]); return (
@@ -456,17 +518,7 @@ const ChatbotDrawer = ({ )}
- {isToggledrawer && !isHelloUser && ( -
- -
- )} - {isHelloUser && CloseButton} + {isToggledrawer && DrawerQuickActionsMenu}
diff --git a/components/Interface-Chatbot/ChatbotHeader.tsx b/components/Interface-Chatbot/ChatbotHeader.tsx index 1b328b13..7d7ed99d 100644 --- a/components/Interface-Chatbot/ChatbotHeader.tsx +++ b/components/Interface-Chatbot/ChatbotHeader.tsx @@ -29,6 +29,7 @@ import { emitEventToParent } from "@/utils/emitEventsToParent/emitEventsToParent import { createRandomId, DEFAULT_AI_SERVICE_MODALS, ParamsEnums } from "@/utils/enums"; import { useChatActions } from "../Chatbot/hooks/useChatActions"; import { ChatbotContext } from "../context"; +import QuickActionsMenu from "./QuickActionsMenu"; import "./InterfaceChatbot.css"; export function ChatbotHeaderPreview() { @@ -609,6 +610,62 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess ); }, [isHelloUser, isChatbotMinimized, fullScreen, toggleFullScreen]) + // Expand button for the collapsed header — un-minimizes the chat back to default size + const ExpandButton = useMemo(() => { + if (!isChatbotMinimized) return null; + return ( +
{ e.stopPropagation(); handleToggleMinimize(); }} + > + +
+ ); + }, [isChatbotMinimized, handleToggleMinimize]) + + // Determine which quick-action items are available + const hasMinimizeAction = !!MinimizeButton; + const hasFullScreenAction = !!ScreenSizeToggleButton && !isFullScreen; + const hasExitFullScreenAction = !!ScreenSizeToggleButton && isFullScreen; + const hasCloseAction = !!CloseButton; + const hasNewConversationAction = !!CreateThreadButton; + + const showQuickActions = ( + hasMinimizeAction || + hasFullScreenAction || + hasExitFullScreenAction || + hasCloseAction || + hasNewConversationAction + ); + + const QuickActionsMenuComponent = useMemo(() => ( + toggleFullScreen(!fullScreen)} + onNewConversation={handleCreateNewSubThread} + onClose={handleCloseChatbot} + position={isChatbotMinimized ? 'top' : 'bottom'} + /> + ), [ + isChatbotMinimized, + fullScreen, + hasMinimizeAction, + hasFullScreenAction, + hasExitFullScreenAction, + hasCloseAction, + hasNewConversationAction, + handleToggleMinimize, + toggleFullScreen, + handleCreateNewSubThread, + handleCloseChatbot + ]) + return isChatbotMinimized ?
@@ -616,8 +673,8 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess {HeaderTitleSection}
-
- {MinimizeButton} +
e.stopPropagation()}> + {ExpandButton} {CloseButton}
@@ -654,10 +711,11 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess ))} - {!isFullScreen &&
- {ScreenSizeToggleButton} - {(isMobileSDK || !isHelloUser) ? CloseButton : MinimizeButton} -
} + {showQuickActions && ( +
+ {QuickActionsMenuComponent} +
+ )}
diff --git a/components/Interface-Chatbot/InterfaceChatbot.css b/components/Interface-Chatbot/InterfaceChatbot.css index 6dc00a92..2eabfd2b 100644 --- a/components/Interface-Chatbot/InterfaceChatbot.css +++ b/components/Interface-Chatbot/InterfaceChatbot.css @@ -36,7 +36,7 @@ padding: 5px !important; border-radius: 50px !important; cursor: pointer !important; - z-index: 999999 !important; + z-index: 99 !important; pointer-events: auto !important; background-color: var(--down-btn-bg-color, #333) !important; color: var(--down-btn-text-color, white) !important; diff --git a/components/Interface-Chatbot/QuickActionsMenu.tsx b/components/Interface-Chatbot/QuickActionsMenu.tsx new file mode 100644 index 00000000..f96d3f14 --- /dev/null +++ b/components/Interface-Chatbot/QuickActionsMenu.tsx @@ -0,0 +1,169 @@ +'use client'; + +import { EllipsisVertical, Maximize2, Minimize2, Minus, Plus, X } from "lucide-react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; + +export interface QuickActionsMenuProps { + /** Open state (controlled). If omitted, the component manages its own state. */ + open?: boolean; + onOpenChange?: (open: boolean) => void; + + isChatbotMinimized?: boolean; + fullScreen?: boolean; + + showMinimize?: boolean; + showFullScreen?: boolean; + showNewConversation?: boolean; + showClose?: boolean; + + onMinimize?: () => void; + onToggleFullScreen?: () => void; + onNewConversation?: () => void; + onClose?: (e?: React.MouseEvent) => void; + + /** Extra class for the trigger button. */ + triggerClassName?: string; + /** Extra class for the menu panel. */ + menuClassName?: string; + /** Icon size for the trigger. */ + triggerIconSize?: number; + /** Apply the `var(--icon-color)` color to the trigger icon. */ + useIconColor?: boolean; + /** Where to anchor the menu relative to the trigger. Defaults to "bottom". */ + position?: "top" | "bottom"; +} + +const QuickActionsMenu: React.FC = ({ + open: controlledOpen, + onOpenChange, + isChatbotMinimized = false, + fullScreen = false, + showMinimize = false, + showFullScreen = false, + showNewConversation = false, + showClose = false, + onMinimize, + onToggleFullScreen, + onNewConversation, + onClose, + triggerClassName = "cursor-pointer p-2 rounded-full hover:bg-gray-200 transition-colors icn", + menuClassName = "absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50 py-1", + triggerIconSize = 22, + useIconColor = false, + position = "bottom", +}) => { + const [internalOpen, setInternalOpen] = useState(false); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? !!controlledOpen : internalOpen; + + const setOpen = (next: boolean) => { + if (!isControlled) setInternalOpen(next); + onOpenChange?.(next); + }; + + const menuRef = useRef(null); + + useEffect(() => { + if (!open) return; + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + const closeMenu = () => setOpen(false); + + const triggerIconProps = useIconColor ? { color: "var(--icon-color)" as const } : {}; + + const trigger = useMemo(() => ( + + // eslint-disable-next-line react-hooks/exhaustive-deps + ), [open, triggerClassName, triggerIconSize, useIconColor]); + + return ( +
+ {trigger} + + {open && ( +
+ {showMinimize && ( + + )} + + {showFullScreen && ( + + )} + + {showNewConversation && ( + + )} + + {showClose && ( + + )} +
+ )} +
+ ); +}; + +export default QuickActionsMenu; From 277b5920203facdba4afff38fbe4bdac8e47dc4f Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Sat, 20 Jun 2026 20:05:22 +0530 Subject: [PATCH 03/11] style: improve sidebar/drawer layout --- .../Interface-Chatbot/ChatbotDrawer.tsx | 68 ++++++++++++------- public/chat-widget-style.css | 2 +- public/chatbot-style.css | 2 +- public/rag.css | 2 +- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index 15eda6b4..e4fdaf0a 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -1,7 +1,7 @@ 'use client'; import { lighten } from "@mui/material"; -import { AlignLeft, ChevronRight, SquarePen, Users } from "lucide-react"; +import { AlignLeft, ChevronRight, SquarePen, Users, Phone, Send } from "lucide-react"; import { useContext, useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; @@ -243,7 +243,9 @@ const ChatbotDrawer = ({ ), [subThreadList, subThreadId, handleChangeSubThread]); const TeamsList = useMemo(() => ( -
+ <> + {((channelList?.length > 0 && channelList.some((thread: any) => thread?.id)) || teamsList?.length > 0) && ( +
{/* Conversations Section */} {(channelList || []).length > 0 && channelList.some((thread: any) => thread?.id) && (
@@ -342,16 +344,12 @@ const ChatbotDrawer = ({ )} {/* Teams Section */} + {(teamsList || []).length > 0 && (

Talk to our experts

- {teamsList.length === 0 ? ( -
- -
- ) : (
{teamsList.map((team: any, index: number) => (
))}
+
+
+ )} +
+ )} + + {/* Voice Call Section */} + {voice_call_widget && ( +
+

Talk to Our Teams

+
+ + {/*Send Message button in case of no team assign */} + { (teamsList || []).length === 0 && ( + )}
- - {voice_call_widget &&
-

Need specialized help?

-

Our teams are ready to assist you with any questions

- -
} -
+ )} + ), [ channelList, teamsList, @@ -439,7 +459,7 @@ const ChatbotDrawer = ({ }; // Quick Actions dropdown for the drawer header - const canMinimize = isHelloUser && !isMobileSDK; + const canMinimize = false; //isHelloUser && !isMobileSDK; const canFullScreen = !isMobileSDK && !isFullScreen; const DrawerQuickActionsMenu = useMemo(() => { @@ -524,10 +544,8 @@ const ChatbotDrawer = ({
{/* Content area with overflow handling - the scrollbar will appear at the edge */} -
-
+
{!isHelloUser ? DrawerList : TeamsList} -
{/* Footer with branding - always stays at bottom */} diff --git a/public/chat-widget-style.css b/public/chat-widget-style.css index f401ce79..0961c549 100644 --- a/public/chat-widget-style.css +++ b/public/chat-widget-style.css @@ -66,7 +66,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; 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; From 9820f98a5e1706f1a1c91a24fff7f024f33c17e5 Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Mon, 22 Jun 2026 19:33:18 +0530 Subject: [PATCH 04/11] Working in Sidebar improvement --- .../Chatbot/hooks/useReduxManagement.ts | 49 ++++++----- .../Interface-Chatbot/ChatbotDrawer.tsx | 83 ++++++++++++++++--- .../Interface-Chatbot/ChatbotHeader.tsx | 7 +- .../Interface-Chatbot/QuickActionsMenu.tsx | 18 +--- .../embeddingScriptEventHandler.ts | 28 ++++++- store/hello/helloReducer.ts | 9 +- 6 files changed, 134 insertions(+), 60 deletions(-) diff --git a/components/Chatbot/hooks/useReduxManagement.ts b/components/Chatbot/hooks/useReduxManagement.ts index 0eb50c7b..b3d9981a 100644 --- a/components/Chatbot/hooks/useReduxManagement.ts +++ b/components/Chatbot/hooks/useReduxManagement.ts @@ -34,25 +34,36 @@ export const useReduxStateManagement = ({ 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, - })); + } = 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]; + })(); + + 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, + currentChatId: state?.appInfo?.[tabSessionId]?.currentChatId || fallbackChat?.id, + currentChannelId: state?.appInfo?.[tabSessionId]?.currentChannelId || fallbackChat?.channel, + currentTeamId: state?.appInfo?.[tabSessionId]?.currentTeamId || fallbackChat?.team_id, + overrideChannelId: state?.appInfo?.[tabSessionId]?.overrideChannelId, + }; + }); return { interfaceContextData, diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index e4fdaf0a..91d1339d 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -1,7 +1,7 @@ 'use client'; import { lighten } from "@mui/material"; -import { AlignLeft, ChevronRight, SquarePen, Users, Phone, Send } from "lucide-react"; +import { AlignLeft, ChevronRight, SquarePen, Users, Phone, Send, X } from "lucide-react"; import { useContext, useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; @@ -67,6 +67,8 @@ const ChatbotDrawer = ({ const { currentChatId, currentTeamId, currentChannelId } = useReduxStateManagement({ chatSessionId, tabSessionId }); const { callState } = useCallUI(); const sendMessageToHello = useOnSendHello(); + const [showAllChannels, setShowAllChannels] = useState(false); + const [showAllTeams, setShowAllTeams] = useState(false); // Consolidated Redux state selection const { @@ -101,7 +103,25 @@ const ChatbotDrawer = ({ }; }); + const VISIBLE_ITEMS_COUNT = 3; + const filteredChannels = (channelList || []).filter( + (channel: any) => channel?.id + ); + + const closedChatsCount = filteredChannels.filter( + (channel: any) => channel?.is_closed + ).length; + + const displayedChannels = showAllChannels + ? filteredChannels + : filteredChannels.slice(0, VISIBLE_ITEMS_COUNT); + + const displayedTeams = showAllTeams + ? teamsList + : teamsList.slice(0, VISIBLE_ITEMS_COUNT); + useEffect(() => { + console.log("filteredChannels", filteredChannels); if (chatSessionId) { setToggleDrawer(true); } @@ -243,9 +263,9 @@ const ChatbotDrawer = ({ ), [subThreadList, subThreadId, handleChangeSubThread]); const TeamsList = useMemo(() => ( - <> - {((channelList?.length > 0 && channelList.some((thread: any) => thread?.id)) || teamsList?.length > 0) && ( -
+ <> + {((channelList?.length > 0 && channelList.some((thread: any) => thread?.id)) || teamsList?.length > 0) && ( +
{/* Conversations Section */} {(channelList || []).length > 0 && channelList.some((thread: any) => thread?.id) && (
@@ -253,9 +273,7 @@ const ChatbotDrawer = ({

Continue Conversations

- {channelList - .filter((channel: any) => channel?.id) - .map((channel: any, index: number) => ( + { displayedChannels.map((channel: any, index: number) => (
))} + {filteredChannels.length > VISIBLE_ITEMS_COUNT && ( +
+ +
+ )}
)} @@ -351,7 +386,7 @@ const ChatbotDrawer = ({
- {teamsList.map((team: any, index: number) => ( + {displayedTeams.map((team: any, index: number) => (
))} + + {teamsList.length > VISIBLE_ITEMS_COUNT && ( +
+ +
+ )}
@@ -463,6 +513,7 @@ const ChatbotDrawer = ({ const canFullScreen = !isMobileSDK && !isFullScreen; const DrawerQuickActionsMenu = useMemo(() => { + if (fullScreen || isFullScreen) return null; if (!isToggledrawer) return null; return ( @@ -472,11 +523,9 @@ const ChatbotDrawer = ({ showMinimize={canMinimize} showFullScreen={canFullScreen} showNewConversation={!isHelloUser} - showClose={!hideCloseButton} onMinimize={handleToggleMinimize} onToggleFullScreen={() => toggleFullScreen(!fullScreen)} onNewConversation={handleCreateNewSubThread} - onClose={() => handleCloseChatbot()} triggerClassName="p-2 hover:bg-gray-200 rounded-full transition-colors icn" menuClassName="absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-[9999] py-1" useIconColor @@ -487,13 +536,12 @@ const ChatbotDrawer = ({ isHelloUser, isChatbotMinimized, fullScreen, + isFullScreen, canMinimize, canFullScreen, - hideCloseButton, handleToggleMinimize, toggleFullScreen, handleCreateNewSubThread, - handleCloseChatbot ]); return ( @@ -537,8 +585,17 @@ const ChatbotDrawer = ({

{tagline}

)}
-
+
{isToggledrawer && DrawerQuickActionsMenu} + {isToggledrawer && !hideCloseButton && ( + + )}
diff --git a/components/Interface-Chatbot/ChatbotHeader.tsx b/components/Interface-Chatbot/ChatbotHeader.tsx index 7d7ed99d..8bd2ca51 100644 --- a/components/Interface-Chatbot/ChatbotHeader.tsx +++ b/components/Interface-Chatbot/ChatbotHeader.tsx @@ -627,14 +627,12 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess const hasMinimizeAction = !!MinimizeButton; const hasFullScreenAction = !!ScreenSizeToggleButton && !isFullScreen; const hasExitFullScreenAction = !!ScreenSizeToggleButton && isFullScreen; - const hasCloseAction = !!CloseButton; const hasNewConversationAction = !!CreateThreadButton; const showQuickActions = ( hasMinimizeAction || hasFullScreenAction || hasExitFullScreenAction || - hasCloseAction || hasNewConversationAction ); @@ -645,11 +643,9 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess showMinimize={hasMinimizeAction} showFullScreen={hasFullScreenAction || hasExitFullScreenAction} showNewConversation={hasNewConversationAction} - showClose={hasCloseAction} onMinimize={handleToggleMinimize} onToggleFullScreen={() => toggleFullScreen(!fullScreen)} onNewConversation={handleCreateNewSubThread} - onClose={handleCloseChatbot} position={isChatbotMinimized ? 'top' : 'bottom'} /> ), [ @@ -658,12 +654,10 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess hasMinimizeAction, hasFullScreenAction, hasExitFullScreenAction, - hasCloseAction, hasNewConversationAction, handleToggleMinimize, toggleFullScreen, handleCreateNewSubThread, - handleCloseChatbot ]) return isChatbotMinimized ? @@ -716,6 +710,7 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess {QuickActionsMenuComponent} )} + {CloseButton} diff --git a/components/Interface-Chatbot/QuickActionsMenu.tsx b/components/Interface-Chatbot/QuickActionsMenu.tsx index f96d3f14..339e3d5a 100644 --- a/components/Interface-Chatbot/QuickActionsMenu.tsx +++ b/components/Interface-Chatbot/QuickActionsMenu.tsx @@ -1,6 +1,6 @@ 'use client'; -import { EllipsisVertical, Maximize2, Minimize2, Minus, Plus, X } from "lucide-react"; +import { EllipsisVertical, Maximize2, Minimize2, Minus, Plus } from "lucide-react"; import React, { useEffect, useMemo, useRef, useState } from "react"; export interface QuickActionsMenuProps { @@ -14,12 +14,10 @@ export interface QuickActionsMenuProps { showMinimize?: boolean; showFullScreen?: boolean; showNewConversation?: boolean; - showClose?: boolean; onMinimize?: () => void; onToggleFullScreen?: () => void; onNewConversation?: () => void; - onClose?: (e?: React.MouseEvent) => void; /** Extra class for the trigger button. */ triggerClassName?: string; @@ -41,11 +39,9 @@ const QuickActionsMenu: React.FC = ({ showMinimize = false, showFullScreen = false, showNewConversation = false, - showClose = false, onMinimize, onToggleFullScreen, onNewConversation, - onClose, triggerClassName = "cursor-pointer p-2 rounded-full hover:bg-gray-200 transition-colors icn", menuClassName = "absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50 py-1", triggerIconSize = 22, @@ -148,18 +144,6 @@ const QuickActionsMenu: React.FC = ({ New conversation )} - - {showClose && ( - - )} )} 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/store/hello/helloReducer.ts b/store/hello/helloReducer.ts index ead35c38..8ce38e03 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] }; } }, From 26301a747b7bd8ac9745049492d71249e3ebaa11 Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Tue, 23 Jun 2026 20:19:22 +0530 Subject: [PATCH 05/11] Working on sidebar design and New Design --- app/globals.css | 2 +- components/Chatbot/hooks/useColor.ts | 14 +++- .../Interface-Chatbot/ChatbotDrawer.tsx | 72 +++++++++++-------- public/chat-widget-local.js | 15 ++-- public/chat-widget-style.css | 16 +++-- utils/themeUtility.js | 18 +++++ utils/utilities.js | 6 +- 7 files changed, 94 insertions(+), 49 deletions(-) diff --git a/app/globals.css b/app/globals.css index a58bdeec..c365b2aa 100644 --- a/app/globals.css +++ b/app/globals.css @@ -9,7 +9,7 @@ --foreground: #171717; --message-card-background: #f0f0f0; --icon-color: #555555; - --drawer-color: #f2f2f2; + --drawer-color: #ffffff; } [data-theme="dark"] { diff --git a/components/Chatbot/hooks/useColor.ts b/components/Chatbot/hooks/useColor.ts index 9b7ad0ce..8a139aa9 100644 --- a/components/Chatbot/hooks/useColor.ts +++ b/components/Chatbot/hooks/useColor.ts @@ -1,10 +1,18 @@ -import { isColorLight } from "@/utils/themeUtility"; +import { getPrimaryGradientBg, isColorLight } 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 isLight = isColorLight(backgroundColor); + const textColor = isLight ? "black" : "white"; - return { backgroundColor, textColor } + return { + backgroundColor, + textColor, + primaryBgColor: backgroundColor, + primaryTextColor: textColor, + foregroundColor: textColor, + primaryGradientBg: getPrimaryGradientBg(backgroundColor), + } } \ No newline at end of file diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index 91d1339d..78fb9705 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -49,7 +49,7 @@ const ChatbotDrawer = ({ threadId }: ChatbotDrawerProps) => { const dispatch = useDispatch(); - const { backgroundColor, textColor } = useColor(); + const { backgroundColor, textColor, primaryGradientBg } = useColor(); // Context hooks const { messageRef } = useContext(MessageContext); @@ -121,7 +121,6 @@ const ChatbotDrawer = ({ : teamsList.slice(0, VISIBLE_ITEMS_COUNT); useEffect(() => { - console.log("filteredChannels", filteredChannels); if (chatSessionId) { setToggleDrawer(true); } @@ -270,7 +269,7 @@ const ChatbotDrawer = ({ {(channelList || []).length > 0 && channelList.some((thread: any) => thread?.id) && (
-

Continue Conversations

+

Continue Conversations

{ displayedChannels.map((channel: any, index: number) => ( @@ -382,20 +381,31 @@ const ChatbotDrawer = ({ {(teamsList || []).length > 0 && (
-

Talk to our experts

+

Talk to our experts

{displayedTeams.map((team: any, index: number) => (
handleChangeTeam(team?.id)} >
-
- {team?.icon || } +
+
+ {team?.name?.charAt(0)?.toUpperCase() || (team?.icon || )} +
+ {team?.widget_unread_count > 0 && ( + + {team?.widget_unread_count} + + )}
+
{team?.name}
@@ -430,7 +440,7 @@ const ChatbotDrawer = ({ {/* Voice Call Section */} {voice_call_widget && (
-

Talk to Our Teams

+

Talk to Our Teams

-
+

{Name ? `Hello ${Name.split(' ')[0]}` : 'Hello There!'}

@@ -601,30 +611,32 @@ const ChatbotDrawer = ({
{/* Content area with overflow handling - the scrollbar will appear at the edge */} -
+
{!isHelloUser ? DrawerList : TeamsList}
{/* Footer with branding - always stays at bottom */} -
-
- {isHelloUser && show_msg91 ? ( - <> - Powered by - - MSG91 - - - ) : !isHelloUser ? ( - <> - Powered by - - GTWY - - - ) : null} + {(isHelloUser && show_msg91) || !isHelloUser ? ( +
+
+ {isHelloUser && show_msg91 ? ( + <> + Powered by + + MSG91 + + + ) : ( + <> + Powered by + + GTWY + + + )} +
-
+ ) : null}
diff --git a/public/chat-widget-local.js b/public/chat-widget-local.js index 9ea03fb7..760758da 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); diff --git a/public/chat-widget-style.css b/public/chat-widget-style.css index 0961c549..b71d6498 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; @@ -80,8 +79,8 @@ [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; @@ -97,7 +96,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 +106,11 @@ right: 18px !important; } +#chatbot-logo { + display: inherit; + place-items: inherit; +} + /* Starter Question Styles */ .hello-starter-question { z-index: 999999; @@ -279,8 +283,8 @@ } .chatbot-icon-interfaceEmbed { - width: 60px !important; - height: 60px !important; + width: 48px !important; + height: 48px !important; cursor: pointer; object-fit: contain; } diff --git a/utils/themeUtility.js b/utils/themeUtility.js index bee75cdb..741b8806 100644 --- a/utils/themeUtility.js +++ b/utils/themeUtility.js @@ -15,4 +15,22 @@ export function isColorLight(color) { // Return true if the color is light, otherwise false return brightness > 128; +} + +export function getPrimaryGradientBg(primaryColor) { + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext("2d"); + ctx.fillStyle = primaryColor; + ctx.fillRect(0, 0, 1, 1); + const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data; + + // Mix white with primary at 8%, 14% and 20% for a smooth 3-stop gradient + const light = (c, mix) => Math.round(255 + (c - 255) * mix); + const r1 = light(r, 0.08), g1 = light(g, 0.08), b1 = light(b, 0.08); + const r2 = light(r, 0.14), g2 = light(g, 0.14), b2 = light(b, 0.14); + const r3 = light(r, 0.20), g3 = light(g, 0.20), b3 = light(b, 0.20); + + return `linear-gradient(150deg, rgb(${r1}, ${g1}, ${b1}) 0%, rgb(${r2}, ${g2}, ${b2}) 50%, rgb(${r3}, ${g3}, ${b3}) 100%)`; } \ No newline at end of file diff --git a/utils/utilities.js b/utils/utilities.js index 60c2c04f..4f9664c1 100644 --- a/utils/utilities.js +++ b/utils/utilities.js @@ -46,8 +46,10 @@ export const generateNewId = (length = 8) => { }; export const generateChannelId = (companyId = '') => { - const uuid = uuidv4().replace(/-/g, ''); - return `ch-comp-${companyId}.${uuid}`; + // Backend regex: ^ch-comp-(\d+)\.([0-9a-f]{32})$ + const numericCompanyId = String(companyId).replace(/\D/g, ''); + const uuid = uuidv4().replace(/-/g, '').toLowerCase(); + return `ch-comp-${numericCompanyId}.${uuid}`; }; function getDomain() { From ee28e7eeb700a8b30a4b6b0cf3fda46d961b1ef6 Mon Sep 17 00:00:00 2001 From: Divyanshu Shrivastava Date: Wed, 24 Jun 2026 00:07:39 +0530 Subject: [PATCH 06/11] refactor(chat): improve channel ID generation logic - Standardize channel IDs to 32-hex format - Implement stricter validation for channel IDs - Ensure new chats trigger on voice call initiation - Sync companyId state via Redux management - Fix legacy ObjectId persistence issues --- .../Chatbot/hooks/useHelloIntegration.ts | 60 ++++++++++++++----- .../Chatbot/hooks/useReduxManagement.ts | 26 ++++++-- .../Interface-Chatbot/ChatbotDrawer.tsx | 36 +++++------ index.html | 7 +++ utils/utilities.js | 1 + 5 files changed, 88 insertions(+), 42 deletions(-) create mode 100644 index.html diff --git a/components/Chatbot/hooks/useHelloIntegration.ts b/components/Chatbot/hooks/useHelloIntegration.ts index 6f5dd6cc..98666111 100644 --- a/components/Chatbot/hooks/useHelloIntegration.ts +++ b/components/Chatbot/hooks/useHelloIntegration.ts @@ -61,13 +61,18 @@ export const useFetchHelloPreviousHistory = () => { const { setChatsLoading } = 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); @@ -92,7 +97,7 @@ export const useFetchHelloPreviousHistory = () => { .finally(() => { setChatsLoading(false); }); - }, [currentChannelId, uuid, setChatsLoading, setHelloMessages, globalDispatch]); + }, [currentChannelId, uuid, companyId, setChatsLoading, setHelloMessages, globalDispatch]); }; export const useGetMoreHelloChats = () => { @@ -101,7 +106,7 @@ export const useGetMoreHelloChats = () => { const { setChatsLoading } = useChatActions(); const { addHelloMessage } = useHelloMessages(); - const { uuid, currentChannelId } = useReduxStateManagement({ + const { uuid, currentChannelId, companyId } = useReduxStateManagement({ chatSessionId, tabSessionId: useHelloContext().tabSessionId }); @@ -112,10 +117,14 @@ 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) + getHelloChatHistoryApi(channelId, skip) .then((response) => { const helloChats = response?.data?.data; if (Array.isArray(helloChats) && helloChats.length > 0) { @@ -136,7 +145,7 @@ export const useGetMoreHelloChats = () => { .finally(() => { setChatsLoading(false); }); - }, [currentChannelId, uuid, setChatsLoading, addHelloMessage, hasMoreMessages, skip, globalDispatch]); + }, [currentChannelId, uuid, companyId, setChatsLoading, addHelloMessage, hasMoreMessages, skip, globalDispatch]); }; export const useFetchChannels = () => { @@ -224,13 +233,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 +319,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 b3d9981a..f2e4b114 100644 --- a/components/Chatbot/hooks/useReduxManagement.ts +++ b/components/Chatbot/hooks/useReduxManagement.ts @@ -33,7 +33,8 @@ export const useReduxStateManagement = ({ currentChannelId, currentTeamId, isDefaultNavigateToChatScreen, - overrideChannelId + overrideChannelId, + companyId } = useCustomSelector((state) => { const channels = state.Hello?.[chatSessionId]?.channelListData?.channels || []; const fallbackChat = (() => { @@ -44,6 +45,13 @@ export const useReduxStateManagement = ({ 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, @@ -58,10 +66,17 @@ export const useReduxStateManagement = ({ 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 || fallbackChat?.id, - currentChannelId: state?.appInfo?.[tabSessionId]?.currentChannelId || fallbackChat?.channel, - currentTeamId: state?.appInfo?.[tabSessionId]?.currentTeamId || fallbackChat?.team_id, + 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 || '', }; }); @@ -82,6 +97,7 @@ export const useReduxStateManagement = ({ currentChatId, currentChannelId, currentTeamId, - overrideChannelId + overrideChannelId, + companyId }; }; \ No newline at end of file diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index 78fb9705..f9ade415 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -194,38 +194,30 @@ const ChatbotDrawer = ({ }; const handleVoiceCall = async () => { - // If no channel is selected, pick the most recent (first valid) channel just for this action - let overrideChannelId; - let overrideChatId; + // Voice call should always start a FRESH chat — never reuse an existing + // chat_id. We only use the team selection (if any) to route the call. let overrideTeamId; - if (!currentChannelId && Array.isArray(channelList) && channelList.length > 0 && channelList?.[0]?.id) { - const firstValid = channelList.find((ch: any) => ch?.id); - if (firstValid) { - overrideChannelId = firstValid?.channel; - overrideChatId = firstValid?.id; - dispatch( - setDataInAppInfoReducer({ - subThreadId: firstValid?.channel, - currentChannelId: firstValid?.channel, - currentChatId: firstValid?.id, - currentTeamId: firstValid?.team_id, - }) - ); - } - } else if (teamsList?.length > 0) { - const firstValid = teamsList[0] + if (Array.isArray(teamsList) && teamsList.length > 0) { + const firstValid = teamsList[0]; if (firstValid) { + overrideTeamId = firstValid?.id; dispatch( setDataInAppInfoReducer({ currentTeamId: firstValid?.id, }) ); - overrideTeamId = firstValid?.id; } } if (isSmallScreen) setToggleDrawer(false); - // pass overrides so sendMessageToHello uses latest values in the same tick - const data = await sendMessageToHello('', '', true, overrideChannelId || currentChannelId, overrideChatId || currentChatId, overrideTeamId || currentTeamId); + // Force a fresh chat by clearing chat_id / channel_id before sending. + dispatch(setDataInAppInfoReducer({ + subThreadId: '', + currentChannelId: '', + currentChatId: '', + overrideChannelId: '', + })); + // Pass empty chatId/channelId overrides so sendMessageToHello creates a new chat. + const data = await sendMessageToHello('', '', true, '', '', overrideTeamId || currentTeamId); helloVoiceService.initiateCall(data?.['call_jwt_token'] || ''); }; diff --git a/index.html b/index.html new file mode 100644 index 00000000..18d10658 --- /dev/null +++ b/index.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/utils/utilities.js b/utils/utilities.js index 4f9664c1..f3263b4d 100644 --- a/utils/utilities.js +++ b/utils/utilities.js @@ -49,6 +49,7 @@ export const generateChannelId = (companyId = '') => { // Backend regex: ^ch-comp-(\d+)\.([0-9a-f]{32})$ const numericCompanyId = String(companyId).replace(/\D/g, ''); const uuid = uuidv4().replace(/-/g, '').toLowerCase(); + console.log("Hero: ", uuid, `ch-comp-${numericCompanyId}.${uuid}`); return `ch-comp-${numericCompanyId}.${uuid}`; }; From de26260c7fe31dbbe5789ea21c8a5d49d7dfd78d Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Wed, 24 Jun 2026 21:14:21 +0530 Subject: [PATCH 07/11] feat(ui): redesign chat and drawer interface - Implement new sidebar conversation list layout - Add relative timestamps to chat threads - Integrate brand footer with MSG91/GTWY support - Refine launcher badge mask and positioning - Update team and conversation styling - Improve header responsiveness and UX --- components/Chatbot/Chatbot.tsx | 38 +- components/FormComponent.tsx | 30 +- .../Interface-Chatbot/ChatbotDrawer.tsx | 361 +++++++++++------- .../Interface-Chatbot/ChatbotHeader.tsx | 4 +- public/chat-widget-local.js | 3 + public/chat-widget-style.css | 23 +- 6 files changed, 297 insertions(+), 162 deletions(-) diff --git a/components/Chatbot/Chatbot.tsx b/components/Chatbot/Chatbot.tsx index 301b4e3c..22bd9c15 100644 --- a/components/Chatbot/Chatbot.tsx +++ b/components/Chatbot/Chatbot.tsx @@ -85,7 +85,7 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { const dispatch = useAppDispatch(); // State management - const { show_widget_form, greetingMessage, isToggledrawer, chatsLoading, messageIds, subThreadId, helloMsgIds } = useCustomSelector((state) => { + const { show_widget_form, greetingMessage, isToggledrawer, chatsLoading, 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, @@ -94,7 +94,8 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { chatsLoading: state.Chat.chatsLoading, 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, }) }); @@ -179,6 +180,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/FormComponent.tsx b/components/FormComponent.tsx index c20c884e..7858dc65 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"; @@ -137,21 +137,21 @@ 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: backgroundColor, color: textColor }} + > +
+
-
- Enter your details -

Click here to provide your information

+
+
Enter your details
+
Click here to provide your information
diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index f9ade415..dd0b82d5 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -1,7 +1,7 @@ 'use client'; import { lighten } from "@mui/material"; -import { AlignLeft, ChevronRight, SquarePen, Users, Phone, Send, X } from "lucide-react"; +import { AlignLeft, ChevronRight, SquarePen, Users, Phone, Send, X, MessageSquareText } from "lucide-react"; import { useContext, useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; @@ -31,6 +31,28 @@ import QuickActionsMenu from "./QuickActionsMenu"; const createRandomId = () => Math.random().toString(36).substring(2, 15); +const formatRelativeTime = (input: any): string => { + if (input === null || input === undefined || input === '') return ''; + let ts: number; + if (typeof input === 'number' || /^\d+$/.test(String(input))) { + const num = Number(input); + // PubNub timetoken is in 100ns units (17-digit). Convert to ms. + ts = num > 1e14 ? Math.floor(num / 10000) : num; + } else { + ts = new Date(input).getTime(); + } + if (!ts || isNaN(ts)) return ''; + const diffSec = Math.floor((Date.now() - ts) / 1000); + if (diffSec < 60) return 'now'; + const diffMin = Math.floor(diffSec / 60); + if (diffMin < 60) return `${diffMin}m`; + const diffHr = Math.floor(diffMin / 60); + if (diffHr < 24) return `${diffHr}h`; + const diffDay = Math.floor(diffHr / 24); + if (diffDay < 7) return `${diffDay}d`; + return `${Math.floor(diffDay / 7)}w`; +}; + interface ChatbotDrawerProps { preview?: boolean; chatSessionId: string @@ -256,115 +278,143 @@ const ChatbotDrawer = ({ const TeamsList = useMemo(() => ( <> {((channelList?.length > 0 && channelList.some((thread: any) => thread?.id)) || teamsList?.length > 0) && ( -
+
{/* Conversations Section */} {(channelList || []).length > 0 && channelList.some((thread: any) => thread?.id) && (
-

Continue Conversations

+

Continue Conversations

-
- { displayedChannels.map((channel: any, index: number) => ( +
+ { displayedChannels.map((channel: any, index: number) => { + const isActive = channel?.id === currentChatId; + const unread = channel?.widget_unread_count || 0; + const isClosed = !!channel?.is_closed; + const channelMessages = allMessages?.[channel?.channel]; + const lastMessageId = channelMessages?.[0]; + const lastMessage = lastMessageId ? allMessagesData?.[channel?.channel]?.[lastMessageId] : null; + + const lastMsgType = channel?.last_message?.message?.message_type + || lastMessage?.messageJson?.message_type + || lastMessage?.message_type; + + const titleByType: Record = { + voice_call: 'Voice Call', + text: 'Chat', + pushNotification: 'Notification', + image: 'Image', + video: 'Video', + file: 'Attachment', + audio: 'Audio', + }; + + const title = channel?.assigned_to?.name + || (lastMsgType && titleByType[lastMsgType]) + || 'Conversation'; + + const subtitleHtml = (() => { + if (lastMessage) { + const isUserMessage = lastMessage?.role == "user" || lastMessage?.role === "voice_call"; + const text = lastMessage?.message_type === 'pushNotification' + ? "Custom Notification" + : (lastMessage.messageJson?.text + || (lastMessage.messageJson?.attachment?.length > 0 ? "Attachment" + : lastMessage.messageJson?.message_type || "New conversation")); + return `${isUserMessage ? "You: " : ""}${text}`; + } + if (channel?.last_message) { + const isYou = !channel?.last_message?.message?.sender_id; + const text = channel?.last_message?.message?.content?.text + || (channel?.last_message?.message?.content?.attachment?.length > 0 ? "Attachment" + : channel?.last_message?.message?.message_type || "New conversation"); + return `${isYou ? "You: " : ""}${text}`; + } + return "New conversation"; + })(); + + const timeLabel = formatRelativeTime( + channel?.last_message?.timetoken + || channel?.last_message?.message?.timetoken + || channel?.last_message?.created_at + || channel?.updated_at + || channel?.last_message_at + || channel?.created_at + ); + + const initials = (() => { + if (channel?.assigned_to?.name) { + const name = channel.assigned_to.name.toString() || ''; + const parts = name.split(' ').filter(Boolean); + if (parts.length > 1) return (parts[0][0] + parts[1][0]).toUpperCase(); + return name.length > 1 + ? (name[0] + name[1]).toUpperCase() + : (name[0] || 'A').toUpperCase(); + } + return 'A'; + })(); + + return (
handleChangeChannel(channel?.channel, channel?.id, channel?.team_id)} > -
- {(() => { - if (channel?.assigned_to?.name) { - const name = channel.assigned_to.name.toString() || ''; - const nameParts = name.split(' '); - if (nameParts.length > 1) { - // If there are multiple words, take first letter of first and second word - return nameParts[0].charAt(0).toUpperCase() + nameParts[1].charAt(0).toUpperCase(); - } else { - // If there's only one word, take first two letters - return name.length > 1 ? - name.charAt(0).toUpperCase() + name.charAt(1).toUpperCase() : - name.charAt(0).toUpperCase(); - } - } else { - return "A"; - } - })()} -
-
- {channel?.channel && allMessages && allMessagesData && ( -
- {(() => { - const channelMessages = allMessages[channel?.channel]; - if (channelMessages && channelMessages?.length > 0) { - const lastMessageId = channelMessages[0]; - const lastMessage = allMessagesData[channel?.channel]?.[lastMessageId]; - if (lastMessage) { - const isUserMessage = lastMessage?.role == "user" || lastMessage?.role === "voice_call"; - return ( - <> - {isUserMessage ? "You: " : "Sender: "} -
0 ? "Attachment" : - lastMessage.messageJson?.message_type || - "New conversation")) - }}>
- - ); - } - } - - // Fallback to channel.last_message if no message found in allMessagesData - if (channel?.last_message) { - return ( - <> - {!channel?.last_message?.message?.sender_id ? "You: " : "Sender: "} -
0 ? "Attachment" : - channel?.last_message?.message?.message_type || - "New conversation") - }}>
- - ); - } - - return "New conversation"; - })()} -
+
+
+ {initials} +
+ {unread > 0 && ( + + {unread} + )}
-
- {channel?.widget_unread_count > 0 && ( -
- {channel?.widget_unread_count} -
+ +
+
+ + {title} + + {isClosed && ( + + Closed + + )} +
+
+
+ +
+ {timeLabel && ( + {timeLabel} )} - +
- ))} - {filteredChannels.length > VISIBLE_ITEMS_COUNT && ( -
- -
- )} + ); + })} + {filteredChannels.length > VISIBLE_ITEMS_COUNT && ( +
+ +
+ )}
)} @@ -373,43 +423,69 @@ const ChatbotDrawer = ({ {(teamsList || []).length > 0 && (
-

Talk to our experts

+

Talk to our teams

-
-
- {displayedTeams.map((team: any, index: number) => ( -
handleChangeTeam(team?.id)} - > -
-
-
- {team?.name?.charAt(0)?.toUpperCase() || (team?.icon || )} +
+
+ {displayedTeams.map((team: any, index: number) => { + const onlineCount = team?.online_users_count ?? team?.online_count ?? team?.online; + const memberCount = team?.members_count ?? team?.total_members ?? team?.member_count; + const initials = (team?.name?.split(' ').filter(Boolean) || []) + .slice(0, 2) + .map((w: string) => w[0]?.toUpperCase()) + .join('') || 'T'; + return ( +
handleChangeTeam(team?.id)} + > +
+
+
+ {initials || (team?.icon || )} +
+ {team?.widget_unread_count > 0 && ( + + {team?.widget_unread_count} + + )}
- {team?.widget_unread_count > 0 && ( - - {team?.widget_unread_count} - - )} -
-
-
{team?.name}
+
+
+ {team?.name} +
+ {(onlineCount != null || memberCount != null) && ( +
+ {onlineCount != null && ( + + + {onlineCount} online + + )} + {onlineCount != null && memberCount != null && ( + · + )} + {memberCount != null && ( + {memberCount} members + )} +
+ )} +
+
+
+
-
- -
-
- ))} + ); + })} {teamsList.length > VISIBLE_ITEMS_COUNT && ( -
+
)} @@ -431,30 +507,37 @@ const ChatbotDrawer = ({ {/* Voice Call Section */} {voice_call_widget && ( -
-

Talk to Our Teams

-
+
+

Need specialized help?

+
{/*Send Message button in case of no team assign */} { (teamsList || []).length === 0 && ( - )} @@ -581,7 +664,7 @@ const ChatbotDrawer = ({

- {Name ? `Hello ${Name.split(' ')[0]}` : 'Hello There!'} + {Name ? `Hello, ${Name.split(' ')[0]} 👋` : 'Hello there! 👋'}

{tagline && (

{tagline}

diff --git a/components/Interface-Chatbot/ChatbotHeader.tsx b/components/Interface-Chatbot/ChatbotHeader.tsx index 8bd2ca51..2263c2a6 100644 --- a/components/Interface-Chatbot/ChatbotHeader.tsx +++ b/components/Interface-Chatbot/ChatbotHeader.tsx @@ -458,7 +458,7 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess // Memoized header title section const HeaderTitleSection = useMemo(() => { - const displayTitle = isChatbotMinimized && lastMessage?.role === 'user' ? 'You' : chatTitle || chatbotTitle || (isHelloUser ? (agentTeamName || teamName || "Conversation")?.toString().split(" ")?.[0] : "AI Assistant"); + const displayTitle = isChatbotMinimized && lastMessage?.role === 'user' ? 'You' : chatTitle || chatbotTitle || (isHelloUser ? (agentTeamName || teamName || "Conversation") : "AI Assistant"); const displaySubtitle = chatSubTitle || chatbotSubtitle || "Do you have any questions? Ask us!"; // Minimized version of the header @@ -476,7 +476,7 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess )}
-

+

{displayTitle}

{unReadCount > 0 && ( diff --git a/public/chat-widget-local.js b/public/chat-widget-local.js index 760758da..91e5c5d5 100644 --- a/public/chat-widget-local.js +++ b/public/chat-widget-local.js @@ -393,12 +393,15 @@ updateBadgeCount(data, channelId) { 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}`); diff --git a/public/chat-widget-style.css b/public/chat-widget-style.css index b71d6498..6025c535 100644 --- a/public/chat-widget-style.css +++ b/public/chat-widget-style.css @@ -86,6 +86,21 @@ 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"] { @@ -422,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 */ From a5cdd4a7090793e05eaa5770276563160c8fe81c Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Thu, 25 Jun 2026 20:02:34 +0530 Subject: [PATCH 08/11] feat(ui): redesign widget with theming, shared fullscreen state, and dev logger - Migrate icon/chip/badge colors from hardcoded blue to theme-derived tints via useColor() and new withAlpha() helper. - Add animate-slideUp / animate-fadeIn keyframes; convert form into a bottom sheet in normal widget mode and a centered dialog in fullscreen mode. - Theme-aware header/drawer icon hover states (headerHoverBg) and input focus rings (theme primary color). - Add dev-only socketLogger that intercepts WS/SSE/fetch/XHR/postMessage traffic under [socket] and exposes window.__socketLogger.history / clear / uninstall. - Refresh drawer conversation preview + relative time immediately on inbound socket messages by passing lastMessage to setUnReadCount. - Keep launcher visible when widget-info fails (hide_launcher defaults to show). - Move fullscreen runtime state into draftData so header + drawer share one source of truth and collapse works on first click from either location. - Add docs/socket-logger.md. Refs: H3126-test --- app/globals.css | 4 + components/Chatbot/Chatbot.tsx | 39 ++- components/Chatbot/hooks/chatReducer.ts | 6 + components/Chatbot/hooks/chatTypes.ts | 4 + components/Chatbot/hooks/useChatActions.ts | 3 +- components/Chatbot/hooks/useColor.ts | 14 +- .../Chatbot/hooks/useHelloIntegration.ts | 25 +- components/Chatbot/utils/socketLogger.ts | 242 ++++++++++++++++++ components/FormComponent.tsx | 49 ++-- .../Interface-Chatbot/ChatbotDrawer.tsx | 112 ++++++-- .../Interface-Chatbot/ChatbotHeader.tsx | 59 +++-- .../Messages/RepliedMessage.tsx | 28 +- .../Interface-Chatbot/QuickActionsMenu.tsx | 19 ++ components/Interface-Chatbot/ReplyPreview.tsx | 16 +- docs/socket-logger.md | 182 +++++++++++++ hooks/socketEventHandler.ts | 21 +- public/chat-widget-local.js | 25 +- store/chat/chatReducerV2.ts | 6 + store/chat/chatSlice.ts | 1 + store/draftData/draftDataSlice.ts | 4 +- store/hello/helloReducer.ts | 53 +++- tailwind.config.ts | 14 + types/reduxCore.ts | 3 +- utils/themeUtility.js | 24 +- 24 files changed, 865 insertions(+), 88 deletions(-) create mode 100644 components/Chatbot/utils/socketLogger.ts create mode 100644 docs/socket-logger.md diff --git a/app/globals.css b/app/globals.css index c365b2aa..a6efe290 100644 --- a/app/globals.css +++ b/app/globals.css @@ -88,4 +88,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 22bd9c15..992cd2dc 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'; @@ -83,15 +90,17 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { const { backgroundColor } = useColor(); const { isSmallScreen } = useScreenSize(); const dispatch = useAppDispatch(); + const retryChats = useRetryChats(); // State management - const { show_widget_form, greetingMessage, isToggledrawer, chatsLoading, messageIds, subThreadId, helloMsgIds, show_msg91 } = 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, @@ -158,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. + +
+ )}
)} 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 8a139aa9..d31ef9d9 100644 --- a/components/Chatbot/hooks/useColor.ts +++ b/components/Chatbot/hooks/useColor.ts @@ -1,4 +1,4 @@ -import { getPrimaryGradientBg, isColorLight } from "@/utils/themeUtility"; +import { getPrimaryGradientBg, isColorLight, withAlpha } from "@/utils/themeUtility"; import { useTheme } from "@mui/material"; export const useColor = () => { @@ -14,5 +14,17 @@ export const useColor = () => { primaryTextColor: textColor, foregroundColor: textColor, primaryGradientBg: getPrimaryGradientBg(backgroundColor), + // Soft translucent tint of the primary color — used for icon bubbles, + // badges, and chip backgrounds so they follow the active theme. + primaryTintColor: withAlpha(backgroundColor, 0.12), + // Slightly stronger primary tint — useful for hover states on neutral + // surfaces (cards, outlined buttons) so the hover reads as on-theme. + primaryHoverTintColor: withAlpha(backgroundColor, 0.08), + // Translucent overlay that reads well on any colored header + // (light theme → near-white wash, dark theme → near-black wash). + // Use as `hover:bg-{headerHoverBg}` for icon buttons in headers. + 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 98666111..da364e9b 100644 --- a/components/Chatbot/hooks/useHelloIntegration.ts +++ b/components/Chatbot/hooks/useHelloIntegration.ts @@ -58,7 +58,7 @@ 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, companyId } = useReduxStateManagement({ @@ -76,6 +76,7 @@ export const useFetchHelloPreviousHistory = () => { if (!channelId || !uuid) return; setChatsLoading(true); + setChatsError(false); getHelloChatHistoryApi(channelId) .then((response) => { const helloChats = response?.data?.data; @@ -93,17 +94,18 @@ export const useFetchHelloPreviousHistory = () => { }) .catch((error) => { console.error("Error fetching Hello chat history:", error); + setChatsError(true); }) .finally(() => { setChatsLoading(false); }); - }, [currentChannelId, uuid, companyId, 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, companyId } = useReduxStateManagement({ @@ -124,6 +126,7 @@ export const useGetMoreHelloChats = () => { if (!channelId || !uuid || !hasMoreMessages) return; setChatsLoading(true); + setChatsError(false); getHelloChatHistoryApi(channelId, skip) .then((response) => { const helloChats = response?.data?.data; @@ -141,11 +144,25 @@ export const useGetMoreHelloChats = () => { }) .catch((error) => { console.error("Error fetching more Hello chat history:", error); + setChatsError(true); }) .finally(() => { setChatsLoading(false); }); - }, [currentChannelId, uuid, companyId, 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 = () => { 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 7858dc65..9f8c777f 100644 --- a/components/FormComponent.tsx +++ b/components/FormComponent.tsx @@ -36,13 +36,17 @@ interface FormErrors { } function FormComponent({ chatSessionId }: FormComponentProps) { - const { textColor, backgroundColor } = useColor(); + const { primaryTextColor, textColor, backgroundColor } = 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({ @@ -157,10 +161,17 @@ function FormComponent({ chatSessionId }: FormComponentProps) {
); return ( -
-
- {/* Card header */} - < div className="bg-primary text-white p-5 rounded-t-lg" style={{ +
setOpen(false)} + > +
e.stopPropagation()} + > + {/* Card header (sticky); drag handle shown only in bottom-sheet mode */} + < div className={`bg-primary text-white px-5 pb-5 rounded-t-2xl sticky top-0 z-10 ${isFullScreen ? 'pt-5' : 'pt-2'}`} style={{ background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, color: textColor }}> @@ -171,11 +182,15 @@ function FormComponent({ chatSessionId }: FormComponentProps) {
{/* Form content */} -
+ {/* Name field */}
@@ -188,7 +203,7 @@ function FormComponent({ chatSessionId }: FormComponentProps) { onChange={handleChange} placeholder="Enter your name" disabled={scriptParams?.name ? true : false} - className={`input input-bordered w-full pl-10 ${errors.name ? "input-error" : "" + className={`input input-bordered focus:outline-none focus:ring-1 focus:border-[var(--theme-primary)] focus:ring-[var(--theme-primary)] w-full pl-10 ${errors.name ? "input-error" : "" }`} required /> @@ -216,7 +231,7 @@ function FormComponent({ chatSessionId }: FormComponentProps) { onChange={handleChange} disabled={scriptParams?.mail || scriptParams?.Email ? true : false} placeholder="Enter your email" - className={`input input-bordered w-full pl-10 ${errors.email ? "input-error" : ""}`} + className={`input input-bordered focus:outline-none focus:ring-1 focus:border-[var(--theme-primary)] focus:ring-[var(--theme-primary)] w-full pl-10 ${errors.email ? "input-error" : ""}`} />
{errors.email && ( @@ -231,13 +246,13 @@ function FormComponent({ chatSessionId }: FormComponentProps) { -
+
{/* Header with padding */}
-
+
{isToggledrawer && ( )}
-
+

{Name ? `Hello, ${Name.split(' ')[0]} 👋` : 'Hello there! 👋'}

@@ -671,10 +728,11 @@ const ChatbotDrawer = ({ )}
- {isToggledrawer && DrawerQuickActionsMenu} - {isToggledrawer && !hideCloseButton && ( + {isToggledrawer && !(fullScreen || isFullScreen) && DrawerQuickActionsMenu} + {isToggledrawer && !(fullScreen || isFullScreen) && !hideCloseButton && ( ); - }, [subThreadList?.length, isHelloUser, isToggledrawer, setToggleDrawer]); + }, [subThreadList?.length, isHelloUser, isToggledrawer, setToggleDrawer, headerHoverBg]); // Memoized create thread button const CreateThreadButton = useMemo(() => { @@ -447,14 +464,15 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess return (
); - }, [showCreateThreadButton, isToggledrawer, handleCreateNewSubThread]); + }, [showCreateThreadButton, isToggledrawer, handleCreateNewSubThread, headerHoverBg]); // Memoized header title section const HeaderTitleSection = useMemo(() => { @@ -555,7 +573,8 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess return fullScreen ? (
toggleFullScreen(false)} > {/* */} @@ -563,14 +582,15 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess
) : (
toggleFullScreen(true)} > {/* */}
); - }, [shouldToggleScreenSize, hideFullScreenButton, fullScreen, toggleFullScreen]); + }, [shouldToggleScreenSize, hideFullScreenButton, fullScreen, toggleFullScreen, headerHoverBg]); // Memoized close button const CloseButton = useMemo(() => { @@ -578,13 +598,14 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess return (
); - }, [hideCloseButton, handleCloseChatbot]); + }, [hideCloseButton, handleCloseChatbot, headerHoverBg]); const handleToggleMinimize = () => { if (!isChatbotMinimized && fullScreen) { @@ -602,26 +623,28 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess if (!isHelloUser) return null; return (
{isChatbotMinimized ? : }
); - }, [isHelloUser, isChatbotMinimized, fullScreen, toggleFullScreen]) + }, [isHelloUser, isChatbotMinimized, fullScreen, toggleFullScreen, headerHoverBg]) // Expand button for the collapsed header — un-minimizes the chat back to default size const ExpandButton = useMemo(() => { if (!isChatbotMinimized) return null; return (
{ e.stopPropagation(); handleToggleMinimize(); }} >
); - }, [isChatbotMinimized, handleToggleMinimize]) + }, [isChatbotMinimized, handleToggleMinimize, headerHoverBg]) // Determine which quick-action items are available const hasMinimizeAction = !!MinimizeButton; @@ -647,6 +670,7 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess onToggleFullScreen={() => toggleFullScreen(!fullScreen)} onNewConversation={handleCreateNewSubThread} position={isChatbotMinimized ? 'top' : 'bottom'} + triggerHoverBg={headerHoverBg} /> ), [ isChatbotMinimized, @@ -658,6 +682,7 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess handleToggleMinimize, toggleFullScreen, handleCreateNewSubThread, + headerHoverBg, ]) return isChatbotMinimized ? diff --git a/components/Interface-Chatbot/Messages/RepliedMessage.tsx b/components/Interface-Chatbot/Messages/RepliedMessage.tsx index 5702c891..cc5f91ad 100644 --- a/components/Interface-Chatbot/Messages/RepliedMessage.tsx +++ b/components/Interface-Chatbot/Messages/RepliedMessage.tsx @@ -2,11 +2,15 @@ import RenderHelloAttachmentMessage from "@/components/Hello/RenderHelloAttachme import RenderHelloInteractiveMessage from "@/components/Hello/RenderHelloInteractiveMessage"; import RenderHelloVedioCallMessage from "@/components/Hello/RenderHelloVedioCallMessage"; import { addUrlDataHoc } from "@/hoc/addUrlDataHoc"; +import { withAlpha } from "@/utils/themeUtility"; import React from "react"; import InterfaceMarkdown from "../Interface-Markdown/InterfaceMarkdown"; +import { useColor } from "../../Chatbot/hooks/useColor"; import { MESSAGE_TYPES } from "./MessageType"; const RepliedMessage = ({ chatSessionId, message }: { chatSessionId: string; message: any }) => { + const { backgroundColor } = useColor(); + if (message?.replied_msg_type !== 'interactive' && !message?.replied_msg_content?.text && !(message?.replied_msg_content?.attachment && message?.replied_msg_content?.attachment?.length > 0)) { @@ -50,9 +54,27 @@ const RepliedMessage = ({ chatSessionId, message }: { chatSessionId: string; mes } } + const isUser = message?.role === 'user'; + // Match the input-area ReplyPreview: a light card with a primary-colored + // accent + sender name. On the primary-colored (outgoing) bubble we use a + // near-opaque white so the purple doesn't bleed through and wash it out; + // on white (incoming) bubbles a soft primary tint reads the same way. + const replyBg = isUser + ? 'rgba(255, 255, 255, 0.92)' + : withAlpha(backgroundColor, 0.08); + const replyBorder = backgroundColor; + return ( -
-
{senderName}
+
+
+ {senderName} +
{message.replied_msg_type === MESSAGE_TYPES.INTERACTIVE ? ( ) : @@ -60,7 +82,7 @@ const RepliedMessage = ({ chatSessionId, message }: { chatSessionId: string; mes ) : message.replied_msg_type === MESSAGE_TYPES.VIDEO_CALL ? () : ( -
+
{(() => { const isBotMessage = senderId && typeof senderId === 'string' && senderId.toLowerCase() !== 'user'; const replyContent = (() => { diff --git a/components/Interface-Chatbot/QuickActionsMenu.tsx b/components/Interface-Chatbot/QuickActionsMenu.tsx index 339e3d5a..0cd849d6 100644 --- a/components/Interface-Chatbot/QuickActionsMenu.tsx +++ b/components/Interface-Chatbot/QuickActionsMenu.tsx @@ -29,6 +29,12 @@ export interface QuickActionsMenuProps { useIconColor?: boolean; /** Where to anchor the menu relative to the trigger. Defaults to "bottom". */ position?: "top" | "bottom"; + /** + * Background color used for the trigger's hover state. Pass a theme-derived + * value (e.g. a translucent overlay) so the wash matches any header color. + * If omitted, the trigger falls back to the `hover:bg-gray-200` class. + */ + triggerHoverBg?: string; } const QuickActionsMenu: React.FC = ({ @@ -47,6 +53,7 @@ const QuickActionsMenu: React.FC = ({ triggerIconSize = 22, useIconColor = false, position = "bottom", + triggerHoverBg, }) => { const [internalOpen, setInternalOpen] = useState(false); const isControlled = controlledOpen !== undefined; @@ -75,6 +82,17 @@ const QuickActionsMenu: React.FC = ({ const triggerIconProps = useIconColor ? { color: "var(--icon-color)" as const } : {}; + const triggerHoverHandlers = triggerHoverBg + ? { + onMouseEnter: (e: React.MouseEvent) => { + (e.currentTarget as HTMLButtonElement).style.backgroundColor = triggerHoverBg; + }, + onMouseLeave: (e: React.MouseEvent) => { + (e.currentTarget as HTMLButtonElement).style.backgroundColor = ''; + }, + } + : {}; + const trigger = useMemo(() => ( diff --git a/components/Interface-Chatbot/ReplyPreview.tsx b/components/Interface-Chatbot/ReplyPreview.tsx index 2d6480cf..5a86c969 100644 --- a/components/Interface-Chatbot/ReplyPreview.tsx +++ b/components/Interface-Chatbot/ReplyPreview.tsx @@ -1,6 +1,7 @@ 'use client'; import { X } from "lucide-react"; import React from "react"; +import { useColor } from "../Chatbot/hooks/useColor"; interface ReplyPreviewProps { replyToMessage: { @@ -15,6 +16,8 @@ interface ReplyPreviewProps { } const ReplyPreview: React.FC = ({ replyToMessage, onCloseReply }) => { + const { backgroundColor, primaryTintColor } = useColor(); + if (!replyToMessage) return null; const getMessageContent = (content: string | { text: string }): string => { @@ -53,10 +56,19 @@ const ReplyPreview: React.FC = ({ replyToMessage, onCloseRepl const displayContent = getDisplayContent(); return ( -
+
- + {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/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/public/chat-widget-local.js b/public/chat-widget-local.js index 91e5c5d5..8805fe83 100644 --- a/public/chat-widget-local.js +++ b/public/chat-widget-local.js @@ -337,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': @@ -392,6 +397,20 @@ } 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 === '*') { @@ -1161,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/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 8ce38e03..279c7524 100644 --- a/store/hello/helloReducer.ts +++ b/store/hello/helloReducer.ts @@ -146,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; @@ -166,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 741b8806..613fdb7e 100644 --- a/utils/themeUtility.js +++ b/utils/themeUtility.js @@ -33,4 +33,26 @@ export function getPrimaryGradientBg(primaryColor) { const r3 = light(r, 0.20), g3 = light(g, 0.20), b3 = light(b, 0.20); return `linear-gradient(150deg, rgb(${r1}, ${g1}, ${b1}) 0%, rgb(${r2}, ${g2}, ${b2}) 50%, rgb(${r3}, ${g3}, ${b3}) 100%)`; -} \ No newline at end of file +} + +// Returns the primary 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 canvas = typeof document !== 'undefined' ? document.createElement('canvas') : null; + let r = 0, g = 0, b = 0; + if (canvas) { + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + const data = ctx.getImageData(0, 0, 1, 1).data; + r = data[0]; g = data[1]; b = data[2]; + } else { + // Fallback for non-browser environments + return color; + } + const a = Math.max(0, Math.min(1, Number(alpha) || 0)); + return `rgba(${r}, ${g}, ${b}, ${a})`; +} From 34742ef2e0c3b0513cc452ed647084f28f2d5cd2 Mon Sep 17 00:00:00 2001 From: Giddh's Black Tiger Date: Fri, 26 Jun 2026 17:33:47 +0530 Subject: [PATCH 09/11] style: design improvement --- app/globals.css | 12 ++++++++++++ components/Chatbot/Chatbot.tsx | 2 +- components/FormComponent.tsx | 6 +++--- components/Interface-Chatbot/ChatbotDrawer.tsx | 16 +++++++++++++--- components/Interface-Chatbot/ChatbotHeader.tsx | 4 ++-- public/chat-widget-local.js | 2 +- 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/globals.css b/app/globals.css index a6efe290..8967e213 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,6 +4,18 @@ @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; diff --git a/components/Chatbot/Chatbot.tsx b/components/Chatbot/Chatbot.tsx index 992cd2dc..53f6d1ae 100644 --- a/components/Chatbot/Chatbot.tsx +++ b/components/Chatbot/Chatbot.tsx @@ -74,7 +74,7 @@ const ActiveChatView = React.memo(() => (
-
+
diff --git a/components/FormComponent.tsx b/components/FormComponent.tsx index 9f8c777f..e38ad39e 100644 --- a/components/FormComponent.tsx +++ b/components/FormComponent.tsx @@ -171,7 +171,7 @@ function FormComponent({ chatSessionId }: FormComponentProps) { onClick={(e) => e.stopPropagation()} > {/* Card header (sticky); drag handle shown only in bottom-sheet mode */} - < div className={`bg-primary text-white px-5 pb-5 rounded-t-2xl sticky top-0 z-10 ${isFullScreen ? 'pt-5' : 'pt-2'}`} style={{ + < div className={`bg-primary text-white px-5 py-4 rounded-t-2xl sticky top-0 z-10`} style={{ background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, color: textColor }}> @@ -189,7 +189,7 @@ function FormComponent({ chatSessionId }: FormComponentProps) { > {/* Name field */}
-