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/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 0eb50c7b..f2e4b114 100644 --- a/components/Chatbot/hooks/useReduxManagement.ts +++ b/components/Chatbot/hooks/useReduxManagement.ts @@ -33,26 +33,52 @@ export const useReduxStateManagement = ({ currentChannelId, currentTeamId, isDefaultNavigateToChatScreen, - overrideChannelId - } = useCustomSelector((state) => ({ - interfaceContextData: state.Interface?.[chatSessionId]?.interfaceContext?.variables, - isHelloUser: state.draftData?.isHelloUser || false, - uuid: state.Hello?.[chatSessionId]?.channelListData?.uuid, - unique_id: state.Hello?.[chatSessionId]?.channelListData?.unique_id, - presence_channel: state.Hello?.[chatSessionId]?.channelListData?.presence_channel, - team_id: state.Hello?.[chatSessionId]?.widgetInfo?.team?.[0]?.id, - isDefaultNavigateToChatScreen: isDefaultNavigateToChatScreenFn(state, chatSessionId), - chat_id: state.Hello?.[chatSessionId]?.Channel?.id, - channelId: state.Hello?.[chatSessionId]?.Channel?.channel || null, - mode: state.Hello?.[chatSessionId]?.mode || [], - selectedAiServiceAndModal: state.Interface?.[chatSessionId]?.selectedAiServiceAndModal || null, - unique_id_hello: state?.Hello?.[chatSessionId]?.helloConfig?.unique_id, - widgetToken: state?.Hello?.[chatSessionId]?.helloConfig?.widgetToken, - currentChatId: state?.appInfo?.[tabSessionId]?.currentChatId, - currentChannelId: state?.appInfo?.[tabSessionId]?.currentChannelId, - currentTeamId: state?.appInfo?.[tabSessionId]?.currentTeamId, - overrideChannelId: state?.appInfo?.[tabSessionId]?.overrideChannelId, - })); + overrideChannelId, + companyId + } = useCustomSelector((state) => { + const channels = state.Hello?.[chatSessionId]?.channelListData?.channels || []; + const fallbackChat = (() => { + if (!channels?.length) return null; + const openChats = channels + .filter((ch: any) => !ch.is_closed) + .sort((a: any, b: any) => (b.last_message?.timetoken || 0) - (a.last_message?.timetoken || 0)); + return openChats[0] || channels[0]; + })(); + + // Treat "" / null / undefined as a deliberate "start fresh" signal, + // not as a fallback trigger. Only fall back when explicitly cleared. + const isExplicitlyEmpty = (v: any) => v === '' || v === null || v === undefined; + const appInfoChannelId = state?.appInfo?.[tabSessionId]?.currentChannelId; + const appInfoChatId = state?.appInfo?.[tabSessionId]?.currentChatId; + const appInfoTeamId = state?.appInfo?.[tabSessionId]?.currentTeamId; + + return { + interfaceContextData: state.Interface?.[chatSessionId]?.interfaceContext?.variables, + isHelloUser: state.draftData?.isHelloUser || false, + uuid: state.Hello?.[chatSessionId]?.channelListData?.uuid, + unique_id: state.Hello?.[chatSessionId]?.channelListData?.unique_id, + presence_channel: state.Hello?.[chatSessionId]?.channelListData?.presence_channel, + team_id: state.Hello?.[chatSessionId]?.widgetInfo?.team?.[0]?.id, + isDefaultNavigateToChatScreen: isDefaultNavigateToChatScreenFn(state, chatSessionId), + chat_id: state.Hello?.[chatSessionId]?.Channel?.id, + channelId: state.Hello?.[chatSessionId]?.Channel?.channel || null, + mode: state.Hello?.[chatSessionId]?.mode || [], + selectedAiServiceAndModal: state.Interface?.[chatSessionId]?.selectedAiServiceAndModal || null, + unique_id_hello: state?.Hello?.[chatSessionId]?.helloConfig?.unique_id, + widgetToken: state?.Hello?.[chatSessionId]?.helloConfig?.widgetToken, + currentChannelId: isExplicitlyEmpty(appInfoChannelId) + ? '' + : (appInfoChannelId ?? fallbackChat?.channel ?? ''), + currentChatId: isExplicitlyEmpty(appInfoChatId) + ? '' + : (appInfoChatId ?? fallbackChat?.id ?? ''), + currentTeamId: isExplicitlyEmpty(appInfoTeamId) + ? '' + : (appInfoTeamId ?? fallbackChat?.team_id ?? ''), + overrideChannelId: state?.appInfo?.[tabSessionId]?.overrideChannelId, + companyId: state.Hello?.[chatSessionId]?.widgetInfo?.company_id || '', + }; + }); return { interfaceContextData, @@ -71,6 +97,7 @@ export const useReduxStateManagement = ({ currentChatId, currentChannelId, currentTeamId, - overrideChannelId + overrideChannelId, + companyId }; }; \ No newline at end of file diff --git a/components/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..f9ade415 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, Phone, Send, X } 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); @@ -46,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); @@ -64,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 { @@ -75,9 +80,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,10 +96,30 @@ 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 }; }); + 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(() => { if (chatSessionId) { setToggleDrawer(true); @@ -165,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'] || ''); }; @@ -233,17 +254,17 @@ 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) && (
-

Continue Conversations

+

Continue Conversations

- {channelList - .filter((channel: any) => channel?.id) - .map((channel: any, index: number) => ( + { displayedChannels.map((channel: any, index: number) => (
))} + {filteredChannels.length > VISIBLE_ITEMS_COUNT && ( +
+ +
+ )}
)} {/* Teams Section */} + {(teamsList || []).length > 0 && (
-

Talk to our experts

+

Talk to our experts

- {teamsList.length === 0 ? ( -
- -
- ) : (
- {teamsList.map((team: any, index: number) => ( + {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}
@@ -362,24 +407,61 @@ const ChatbotDrawer = ({
))} + + {teamsList.length > VISIBLE_ITEMS_COUNT && ( +
+ +
+ )}
+
+
+ )} +
+ )} + + {/* 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, @@ -401,18 +483,68 @@ 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 = false; //isHelloUser && !isMobileSDK; + const canFullScreen = !isMobileSDK && !isFullScreen; + + const DrawerQuickActionsMenu = useMemo(() => { + if (fullScreen || isFullScreen) return null; + if (!isToggledrawer) return null; return ( -
- -
+ toggleFullScreen(!fullScreen)} + onNewConversation={handleCreateNewSubThread} + 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, + isFullScreen, + canMinimize, + canFullScreen, + handleToggleMinimize, + toggleFullScreen, + handleCreateNewSubThread, + ]); return (
@@ -435,8 +567,8 @@ const ChatbotDrawer = ({
{/* Header with padding */} -
-
+
+
{isToggledrawer && (
-
+

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

@@ -455,49 +587,48 @@ const ChatbotDrawer = ({

{tagline}

)}
-
- {isToggledrawer && !isHelloUser && ( -
- -
+
+ {isToggledrawer && DrawerQuickActionsMenu} + {isToggledrawer && !hideCloseButton && ( + )} - {isHelloUser && CloseButton}
{/* 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/components/Interface-Chatbot/ChatbotHeader.tsx b/components/Interface-Chatbot/ChatbotHeader.tsx index 1b328b13..8bd2ca51 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,56 @@ 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 hasNewConversationAction = !!CreateThreadButton; + + const showQuickActions = ( + hasMinimizeAction || + hasFullScreenAction || + hasExitFullScreenAction || + hasNewConversationAction + ); + + const QuickActionsMenuComponent = useMemo(() => ( + toggleFullScreen(!fullScreen)} + onNewConversation={handleCreateNewSubThread} + position={isChatbotMinimized ? 'top' : 'bottom'} + /> + ), [ + isChatbotMinimized, + fullScreen, + hasMinimizeAction, + hasFullScreenAction, + hasExitFullScreenAction, + hasNewConversationAction, + handleToggleMinimize, + toggleFullScreen, + handleCreateNewSubThread, + ]) + return isChatbotMinimized ?
@@ -616,8 +667,8 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess {HeaderTitleSection}
-
- {MinimizeButton} +
e.stopPropagation()}> + {ExpandButton} {CloseButton}
@@ -654,10 +705,12 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess ))} - {!isFullScreen &&
- {ScreenSizeToggleButton} - {(isMobileSDK || !isHelloUser) ? CloseButton : MinimizeButton} -
} + {showQuickActions && ( +
+ {QuickActionsMenuComponent} +
+ )} + {CloseButton}
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..339e3d5a --- /dev/null +++ b/components/Interface-Chatbot/QuickActionsMenu.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { EllipsisVertical, Maximize2, Minimize2, Minus, Plus } 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; + + onMinimize?: () => void; + onToggleFullScreen?: () => void; + onNewConversation?: () => 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, + onMinimize, + onToggleFullScreen, + onNewConversation, + 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 && ( + + )} +
+ )} +
+ ); +}; + +export default QuickActionsMenu; 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/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/package.json b/package.json index d894f90e..5c642e85 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev -p 3001 --turbopack", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/public/chat-widget-local.js b/public/chat-widget-local.js index 9ea03fb7..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 f401ce79..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; @@ -66,7 +65,7 @@ z-index: 2147483647; display: none; box-sizing: border-box; - border-radius: 12px; + border-radius: 16px; overflow: hidden; border: 1px solid #cecece; box-shadow: rgba(15, 15, 15, 0.08) 0px 5px 40px 0px; @@ -80,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/public/chatbot-style.css b/public/chatbot-style.css index 964a92a4..7aeed2ea 100644 --- a/public/chatbot-style.css +++ b/public/chatbot-style.css @@ -63,7 +63,7 @@ z-index: 9999; display: none; box-sizing: border-box; - border-radius: 12px; + border-radius: 16px; overflow: hidden; border: 1px solid #cecece; } diff --git a/public/rag.css b/public/rag.css index 8077266e..6e348e6e 100644 --- a/public/rag.css +++ b/public/rag.css @@ -436,7 +436,7 @@ height: 90vh; max-width: 1200px; max-height: 800px; - border-radius: 12px; + border-radius: 16px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); overflow: hidden; diff --git a/store/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] }; } }, 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..f3263b4d 100644 --- a/utils/utilities.js +++ b/utils/utilities.js @@ -46,8 +46,11 @@ 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(); + console.log("Hero: ", uuid, `ch-comp-${numericCompanyId}.${uuid}`); + return `ch-comp-${numericCompanyId}.${uuid}`; }; function getDomain() {