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}
+
+ )}
+
@@ -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
-
-
-
- >
- ) : !isHelloUser ? (
- <>
- Powered by
-
- GTWY
-
- >
- ) : null}
+ {(isHelloUser && show_msg91) || !isHelloUser ? (
+
+
+ {isHelloUser && show_msg91 ? (
+ <>
+ Powered by
+
+
+
+ >
+ ) : (
+ <>
+ 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() {