From 9b07412456af4cc86c88aa230cc1549108f857bd Mon Sep 17 00:00:00 2001 From: Divyanshu Date: Tue, 30 Jun 2026 19:36:16 +0530 Subject: [PATCH 1/4] Hide scrollbars globally and update color system with new theming variables - Hide scrollbars across all elements using CSS - Update drawer background color from #f2f2f2 to #ffffff - Remove outline from input and select elements - Refactor useColor hook to return comprehensive color palette including gradients, tints, and hover states - Rename `backgroundColor` to `primaryBgColor` and `textColor` to `foregroundColor` throughout components - Update all components to use new color naming convention (Chatbot, FormComponent, R --- app/globals.css | 18 +- components/Chatbot/Chatbot.tsx | 4 +- components/Chatbot/hooks/useColor.ts | 20 +- components/FormComponent.tsx | 14 +- .../Hello/RenderHelloInteractiveMessage.tsx | 14 +- components/Interface-Chatbot/CallButton.tsx | 6 +- .../Interface-Chatbot/ChatbotDrawer.tsx | 14 +- .../Interface-Chatbot/InterfaceChatbot.css | 2 +- .../Interface-Chatbot/Messages/DateGroup.tsx | 2 +- .../Interface-Chatbot/Messages/Message.tsx | 20 +- .../Messages/MessageList.tsx | 10 +- .../Messages/UserMessage.tsx | 6 +- .../Interface-Chatbot/MoveToDownButton.tsx | 4 +- .../Interface-Chatbot/QuickActionsMenu.tsx | 172 ++++++++++++++++++ package.json | 2 +- public/chat-widget-style.css | 41 +++-- public/chatbot-style.css | 2 +- public/rag.css | 2 +- store/hello/helloReducer.ts | 9 +- tailwind.config.ts | 15 ++ utils/themeUtility.js | 59 +++++- 21 files changed, 360 insertions(+), 76 deletions(-) create mode 100644 components/Interface-Chatbot/QuickActionsMenu.tsx diff --git a/app/globals.css b/app/globals.css index a58bdeec..8967e213 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,12 +4,24 @@ @plugin "daisyui"; @plugin "@tailwindcss/typography"; +/* Hide Scrollbar */ +* { + scrollbar-width: none; +} + +::-webkit-scrollbar { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + display: none; +} + :root { --background: #ffffff; --foreground: #171717; --message-card-background: #f0f0f0; --icon-color: #555555; - --drawer-color: #f2f2f2; + --drawer-color: #ffffff; } [data-theme="dark"] { @@ -88,4 +100,8 @@ html:has(.drawer-toggle:checked) { [data-theme="dark"] .icn { @apply hover:bg-gray-800 hover:text-gray-200 +} + +input, select { + outline: none !important; } \ No newline at end of file diff --git a/components/Chatbot/Chatbot.tsx b/components/Chatbot/Chatbot.tsx index 301b4e3c..88c8f3cc 100644 --- a/components/Chatbot/Chatbot.tsx +++ b/components/Chatbot/Chatbot.tsx @@ -80,7 +80,7 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { const messageRef = useRef(null); const timeoutIdRef = useRef(null); - const { backgroundColor } = useColor(); + const { primaryBgColor } = useColor(); const { isSmallScreen } = useScreenSize(); const dispatch = useAppDispatch(); @@ -162,7 +162,7 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) {
)} diff --git a/components/Chatbot/hooks/useColor.ts b/components/Chatbot/hooks/useColor.ts index 9b7ad0ce..88192897 100644 --- a/components/Chatbot/hooks/useColor.ts +++ b/components/Chatbot/hooks/useColor.ts @@ -1,10 +1,22 @@ -import { isColorLight } from "@/utils/themeUtility"; +import { getGradientBackground, isColorLight, withAlpha } from "@/utils/themeUtility"; import { useTheme } from "@mui/material"; export const useColor = () => { const theme = useTheme(); - const backgroundColor = theme.palette.primary.main; - const textColor = isColorLight(backgroundColor) ? "black" : "white"; + const primaryColor = theme.palette.primary.main; + const isLight = isColorLight(primaryColor); + const textColor = isLight ? "black" : "white"; - return { backgroundColor, textColor } + return { + primaryBgColor: primaryColor, + // Kept separate from primaryBgColor so the text color can be customized independently in the future. + primaryTextColor: primaryColor, + foregroundColor: textColor, + primaryGradientBg: getGradientBackground(primaryColor), + primaryTintColor: withAlpha(primaryColor, 0.12), + primaryHoverTintColor: withAlpha(primaryColor, 0.08), + headerHoverBg: isLight + ? "rgba(0, 0, 0, 0.08)" + : "rgba(255, 255, 255, 0.18)", + } } \ No newline at end of file diff --git a/components/FormComponent.tsx b/components/FormComponent.tsx index 98713c5d..2de477df 100644 --- a/components/FormComponent.tsx +++ b/components/FormComponent.tsx @@ -36,7 +36,7 @@ interface FormErrors { } function FormComponent({ chatSessionId }: FormComponentProps) { - const { textColor, backgroundColor } = useColor(); + const { foregroundColor, primaryBgColor } = useColor(); const dispatch = useDispatch(); const { showWidgetForm, open, userData } = useCustomSelector((state) => ({ showWidgetForm: state.Hello?.[chatSessionId]?.showWidgetForm ?? true, @@ -141,8 +141,8 @@ function FormComponent({ chatSessionId }: FormComponentProps) { className={`bg-white p-2 px-4 cursor-pointer z-[9999] hover:shadow-md transition-all mx-auto rounded-br-md rounded-bl-md ${isSmallScreen ? 'w-full' : 'w-1/2 max-w-lg'}`} onClick={() => setOpen(true)} style={{ - background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, - color: textColor + background: `linear-gradient(to right, ${primaryBgColor}, ${primaryBgColor}CC)`, + color: foregroundColor }} >
@@ -161,8 +161,8 @@ function FormComponent({ chatSessionId }: FormComponentProps) {
{/* Card header */} < div className="bg-primary text-white p-5 rounded-t-lg" style={{ - background: `linear-gradient(to right, ${backgroundColor}, ${backgroundColor}CC)`, - color: textColor + background: `linear-gradient(to right, ${primaryBgColor}, ${primaryBgColor}CC)`, + color: foregroundColor }}>

Enter your details

@@ -288,8 +288,8 @@ function FormComponent({ chatSessionId }: FormComponentProps) { className="btn flex-1" style={{ opacity: isLoading ? 0.5 : 1, - backgroundColor: backgroundColor, - color: textColor + backgroundColor: primaryBgColor, + color: foregroundColor }} > {isLoading ? ( diff --git a/components/Hello/RenderHelloInteractiveMessage.tsx b/components/Hello/RenderHelloInteractiveMessage.tsx index f752f3bb..7767faa7 100644 --- a/components/Hello/RenderHelloInteractiveMessage.tsx +++ b/components/Hello/RenderHelloInteractiveMessage.tsx @@ -7,7 +7,7 @@ import ImageWithFallback from '../Interface-Chatbot/Messages/ImageWithFallback'; function RenderHelloInteractiveMessage({ message }: { message: any }) { const messageJson = message?.messageJson || {}; const sendMessageToHello = useSendMessageToHello({}); - const { textColor, backgroundColor } = useColor(); + const { foregroundColor, primaryBgColor } = useColor(); const renderHeader = (header: any) => { if (header?.type === "text") { @@ -67,7 +67,7 @@ function RenderHelloInteractiveMessage({ message }: { message: any }) { target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium w-full max-w-md justify-start" - style={{ backgroundColor: backgroundColor, color: textColor }} + style={{ backgroundColor: primaryBgColor, color: foregroundColor }} onClick={(e) => e.stopPropagation()} > @@ -125,7 +125,7 @@ function RenderHelloInteractiveMessage({ message }: { message: any }) { target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium" - style={{ backgroundColor: backgroundColor, color: textColor }} + style={{ backgroundColor: primaryBgColor, color: foregroundColor }} > {action.parameters.display_text || "View"} @@ -143,7 +143,7 @@ function RenderHelloInteractiveMessage({ message }: { message: any }) { target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium w-full max-w-md justify-start" - style={{ backgroundColor: backgroundColor, color: textColor }} + style={{ backgroundColor: primaryBgColor, color: foregroundColor }} onClick={(e) => e.stopPropagation()} > @@ -224,7 +224,7 @@ function RenderHelloInteractiveMessage({ message }: { message: any }) { @@ -329,7 +329,7 @@ function RenderHelloInteractiveMessage({ message }: { message: any }) { ); } -function CarouselMessage({ messageJson, backgroundColor, textColor, sendMessageToHello, renderHeader }: any) { +function CarouselMessage({ messageJson, backgroundColor, foregroundColor, sendMessageToHello, renderHeader }: any) { return (

{messageJson.body?.text && ( @@ -358,7 +358,7 @@ function CarouselMessage({ messageJson, backgroundColor, textColor, sendMessageT target="_blank" rel="noopener noreferrer" className="btn btn-sm rounded-md normal-case font-medium flex items-center justify-center gap-2" - style={{ backgroundColor, color: textColor, border: "none" }} + style={{ backgroundColor, color: foregroundColor, border: "none" }} onClick={(e) => e.stopPropagation()} > diff --git a/components/Interface-Chatbot/CallButton.tsx b/components/Interface-Chatbot/CallButton.tsx index bf1fdb73..b49b5613 100644 --- a/components/Interface-Chatbot/CallButton.tsx +++ b/components/Interface-Chatbot/CallButton.tsx @@ -21,7 +21,7 @@ function CallButton({ chatSessionId, currentChannelId }: CallButtonProps) { voice_call_widget: state.Hello?.[chatSessionId]?.widgetInfo?.voice_call_widget || false, })); const sendMessageToHello = useOnSendHello(); - const { backgroundColor } = useColor(); + const { primaryBgColor } = useColor(); const { callState } = useCallUI(); // Handler for voice call @@ -50,10 +50,10 @@ function CallButton({ chatSessionId, currentChannelId }: CallButtonProps) { ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-gray-200" }`} - style={{ backgroundColor: lighten(backgroundColor, 0.8) }} + style={{ backgroundColor: lighten(primaryBgColor, 0.8) }} onClick={() => { if (!isCallDisabled) handleVoiceCall() }} > - +
); } diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index 1a036ae8..1294eb1d 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -46,7 +46,7 @@ const ChatbotDrawer = ({ threadId }: ChatbotDrawerProps) => { const dispatch = useDispatch(); - const { backgroundColor, textColor } = useColor(); + const { primaryBgColor, foregroundColor } = useColor(); // Context hooks const { messageRef } = useContext(MessageContext); @@ -248,11 +248,11 @@ const ChatbotDrawer = ({ key={`${channel?._id}-${index}`} className={`conversation-card max-h-16 h-full overflow-hidden text-ellipsis p-3 ${channel?.id === currentChatId ? 'border-2 border-primary' : ''} bg-white dark:bg-[var(--background)] rounded-lg shadow-sm hover:shadow-md transition-all cursor-pointer flex items-center`} style={{ - borderColor: channel?.id === currentChatId ? backgroundColor : '' + borderColor: channel?.id === currentChatId ? primaryBgColor : '' }} onClick={() => handleChangeChannel(channel?.channel, channel?.id, channel?.team_id)} > -
+
{(() => { if (channel?.assigned_to?.name) { const name = channel.assigned_to.name.toString() || ''; @@ -319,7 +319,7 @@ const ChatbotDrawer = ({
{channel?.widget_unread_count > 0 && ( -
+
{channel?.widget_unread_count}
)} @@ -339,7 +339,7 @@ const ChatbotDrawer = ({
{teamsList.length === 0 ? (
- +
) : (
@@ -372,7 +372,7 @@ const ChatbotDrawer = ({

Our teams are ready to assist you with any questions

diff --git a/components/Interface-Chatbot/Messages/UserMessage.tsx b/components/Interface-Chatbot/Messages/UserMessage.tsx index e5ccdcef..01be5f4f 100644 --- a/components/Interface-Chatbot/Messages/UserMessage.tsx +++ b/components/Interface-Chatbot/Messages/UserMessage.tsx @@ -16,7 +16,7 @@ import RepliedMessage from './RepliedMessage'; * It includes an image with fallback, message content, and sender time. */ -const UserMessageCard = React.memo(({ message, backgroundColor, textColor, chatSessionId }: any) => { +const UserMessageCard = React.memo(({ message, backgroundColor, foregroundColor, chatSessionId }: any) => { const [showSenderTime, setShowSenderTime] = useState(false); const [showReplyButton, setShowReplyButton] = useState(false); const { setReplyToMessage } = useReplyContext(); @@ -65,7 +65,7 @@ const UserMessageCard = React.memo(({ message, backgroundColor, textColor, chatS className="p-2.5 min-w-[40px] sm:max-w-[80%] max-w-[90%] rounded-[10px_10px_1px_10px] break-words user-message-bubble" style={{ backgroundColor: backgroundColor, - color: textColor + color: foregroundColor }} onClick={() => setShowSenderTime(!showSenderTime)} > @@ -84,7 +84,7 @@ const UserMessageCard = React.memo(({ message, backgroundColor, textColor, chatS alt={`Image ${index + 1}`} style={{ backgroundColor: backgroundColor, - color: textColor, + color: foregroundColor, borderRadius: "10px", maxHeight: "300px", }} diff --git a/components/Interface-Chatbot/MoveToDownButton.tsx b/components/Interface-Chatbot/MoveToDownButton.tsx index 66c08bd6..adaff3b5 100644 --- a/components/Interface-Chatbot/MoveToDownButton.tsx +++ b/components/Interface-Chatbot/MoveToDownButton.tsx @@ -3,7 +3,7 @@ import { IconButton } from "@mui/material"; import React from "react"; import "./InterfaceChatbot.css"; -function MoveToDownButton({ movetoDown, showScrollButton, backgroundColor, textColor }: { movetoDown: () => void, showScrollButton: boolean, backgroundColor: string, textColor: string }) { +function MoveToDownButton({ movetoDown, showScrollButton, backgroundColor, foregroundColor }: { movetoDown: () => void, showScrollButton: boolean, backgroundColor: string, foregroundColor: string }) { if (!showScrollButton) return null; return ( diff --git a/components/Interface-Chatbot/QuickActionsMenu.tsx b/components/Interface-Chatbot/QuickActionsMenu.tsx new file mode 100644 index 00000000..cebc7721 --- /dev/null +++ b/components/Interface-Chatbot/QuickActionsMenu.tsx @@ -0,0 +1,172 @@ +'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"; + /** + * 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 = ({ + 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", + triggerHoverBg, +}) => { + 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 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(() => ( + + // eslint-disable-next-line react-hooks/exhaustive-deps + ), [open, triggerClassName, triggerIconSize, useIconColor]); + + return ( +
+ {trigger} + + {open && ( +
+ {showMinimize && ( + + )} + + {showFullScreen && ( + + )} + + {showNewConversation && ( + + )} +
+ )} +
+ ); +}; + +export default QuickActionsMenu; \ 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-style.css b/public/chat-widget-style.css index f401ce79..1c062385 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; @@ -78,10 +77,11 @@ } [id$="-hello-chatbot-icon-image"] { + position: relative; 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; @@ -93,11 +93,25 @@ z-index: 99999; } +/* 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-iframe-parent-container"].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-chatbot-launcher-icon"] { /* background-color: transparent; */ object-fit: contain; /* cursor: pointer; */ - z-index: 9999 !important; + z-index: 2147483003 !important; height: auto; width: auto; box-sizing: border-box !important; @@ -107,6 +121,11 @@ right: 18px !important; } +#chatbot-logo { + display: inherit; + place-items: inherit; +} + /* Starter Question Styles */ .hello-starter-question { z-index: 999999; @@ -279,8 +298,8 @@ } .chatbot-icon-interfaceEmbed { - width: 60px !important; - height: 60px !important; + width: 48px !important; + height: 48px !important; cursor: pointer; object-fit: contain; } @@ -418,12 +437,12 @@ .hello-badge-count { /* Dimensions & Position */ - min-width: 20px; - height: 20px; + min-width: 18px; + height: 18px; position: absolute; top: -2px; - right: 5px; - padding: 4px; + right: -11px; + padding: 3px; box-sizing: border-box; /* Centering */ diff --git a/public/chatbot-style.css b/public/chatbot-style.css index 964a92a4..7aeed2ea 100644 --- a/public/chatbot-style.css +++ b/public/chatbot-style.css @@ -63,7 +63,7 @@ z-index: 9999; display: none; box-sizing: border-box; - border-radius: 12px; + border-radius: 16px; overflow: hidden; border: 1px solid #cecece; } diff --git a/public/rag.css b/public/rag.css index 8077266e..6e348e6e 100644 --- a/public/rag.css +++ b/public/rag.css @@ -436,7 +436,7 @@ height: 90vh; max-width: 1200px; max-height: 800px; - border-radius: 12px; + border-radius: 16px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); overflow: hidden; diff --git a/store/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/tailwind.config.ts b/tailwind.config.ts index 38a3af2a..410be3a5 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -17,6 +17,21 @@ export default { }, }, }, + 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: [ require('daisyui'), require('@tailwindcss/typography'), diff --git a/utils/themeUtility.js b/utils/themeUtility.js index bee75cdb..0a855e9e 100644 --- a/utils/themeUtility.js +++ b/utils/themeUtility.js @@ -1,18 +1,63 @@ -export function isColorLight(color) { - // Create an offscreen canvas for measuring the color brightness +// Mix ratios used for the 3-stop gradient. Adjust here to tweak the look. +const GRADIENT_STOPS = [0.08, 0.14, 0.20]; + +// Parse a CSS color into its RGB components [r, g, b]. +// Returns null when called outside a browser (SSR/tests) so callers can fall back. +function getRgbFromColor(color) { + if (typeof document === "undefined") return null; const canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = 1; - const context = canvas.getContext("2d"); - context.fillStyle = color; - context.fillRect(0, 0, 1, 1); + const ctx = canvas.getContext("2d"); + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data; + return [r, g, b]; +} - // Get the color data (RGBA) of the filled rectangle - const [r, g, b] = context.getImageData(0, 0, 1, 1).data; +// Memoize RGB lookups so repeated calls with the same color skip the canvas work. +const rgbCache = new Map(); +function getRgbCached(color) { + if (rgbCache.has(color)) return rgbCache.get(color); + const rgb = getRgbFromColor(color); + if (rgb) rgbCache.set(color, rgb); + return rgb; +} + +export function isColorLight(color) { + const rgb = getRgbCached(color); + if (!rgb) return false; + const [r, g, b] = rgb; // Calculate brightness (luminance) const brightness = (r * 299 + g * 587 + b * 114) / 1000; // Return true if the color is light, otherwise false return brightness > 128; +} + +export function getGradientBackground(color) { + const rgb = getRgbCached(color); + if (!rgb) return color; + const [r, g, b] = rgb; + + // Mix white with the base color at each stop for a smooth 3-stop gradient. + const mix = (c, ratio) => Math.round(255 + (c - 255) * ratio); + const stops = GRADIENT_STOPS.map((ratio) => { + const [sr, sg, sb] = [mix(r, ratio), mix(g, ratio), mix(b, ratio)]; + return `rgb(${sr}, ${sg}, ${sb})`; + }); + + return `linear-gradient(150deg, ${stops[0]} 0%, ${stops[1]} 50%, ${stops[2]} 100%)`; +} + +// Returns the color as an `rgba(...)` string at the given opacity. +// Useful for soft tints / badges that follow the theme without hardcoding. +export function withAlpha(color, alpha) { + if (color === null || color === undefined) return color; + const rgb = getRgbCached(color); + if (!rgb) return color; + const [r, g, b] = rgb; + const a = Math.max(0, Math.min(1, Number(alpha) || 0)); + return `rgba(${r}, ${g}, ${b}, ${a})`; } \ No newline at end of file From 90439dac2321a9a7e2a849fc1fe9039ae07f2793 Mon Sep 17 00:00:00 2001 From: Divyanshu Date: Wed, 1 Jul 2026 16:09:48 +0530 Subject: [PATCH 2/4] UI/UX improvements --- components/FormComponent.tsx | 83 ++- .../Interface-Chatbot/ChatbotDrawer.tsx | 664 +++++++++++++----- .../Interface-Chatbot/ChatbotHeader.tsx | 128 +++- .../Messages/RepliedMessage.tsx | 28 +- public/chat-widget-local.js | 18 +- public/chat-widget-style.css | 2 +- tailwind.config.ts | 3 +- 7 files changed, 664 insertions(+), 262 deletions(-) diff --git a/components/FormComponent.tsx b/components/FormComponent.tsx index 2de477df..2ce3732d 100644 --- a/components/FormComponent.tsx +++ b/components/FormComponent.tsx @@ -6,7 +6,7 @@ import { setHelloClientInfo, setHelloKeysData } from "@/store/hello/helloSlice"; import { GetSessionStorageData } from "@/utils/ChatbotUtility"; import { useCustomSelector } from "@/utils/deepCheckSelector"; import { splitNumber } from "@/utils/utilities"; -import { BookText, Loader2, Mail, Phone, Send, User } from "lucide-react"; +import { Loader2, Mail, MessageSquare, Phone, Send, User } from "lucide-react"; import React, { useEffect, useState } from "react"; import { useDispatch } from "react-redux"; import { useColor } from "./Chatbot/hooks/useColor"; @@ -36,13 +36,17 @@ interface FormErrors { } function FormComponent({ chatSessionId }: FormComponentProps) { - const { foregroundColor, primaryBgColor } = useColor(); + const { foregroundColor, primaryTextColor, primaryBgColor } = useColor(); const dispatch = useDispatch(); - const { showWidgetForm, open, userData } = useCustomSelector((state) => ({ - showWidgetForm: state.Hello?.[chatSessionId]?.showWidgetForm ?? true, - open: state.Chat.openHelloForm, - userData: state.Hello?.[chatSessionId]?.clientInfo - })); + const { showWidgetForm, open, userData, isFullScreen } = useCustomSelector((state) => { + const fullScreen = state.Hello?.[chatSessionId]?.helloConfig?.fullScreen; + return { + showWidgetForm: state.Hello?.[chatSessionId]?.showWidgetForm ?? true, + open: state.Chat.openHelloForm, + userData: state.Hello?.[chatSessionId]?.clientInfo, + isFullScreen: (fullScreen === true || fullScreen === 'true') ?? false + }; + }); const scriptParams = JSON.parse(GetSessionStorageData('helloConfig') || '{}') const { isSmallScreen } = useScreenSize(); const [formData, setFormData] = useState({ @@ -137,30 +141,37 @@ function FormComponent({ chatSessionId }: FormComponentProps) { if (!open && !showWidgetForm) return null; if (!open && showWidgetForm) return ( -
setOpen(true)} - style={{ - background: `linear-gradient(to right, ${primaryBgColor}, ${primaryBgColor}CC)`, - color: foregroundColor - }} - > -
-
- +
+
setOpen(true)} + className="flex items-center gap-3 rounded-xl px-3 py-2.5 cursor-pointer transition-all hover:opacity-95 hover:shadow-md" + style={{ backgroundColor: primaryBgColor, color: foregroundColor }} + > +
+
-
- Enter your details -

Click here to provide your information

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