import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import type { FC } from 'react'; import type { IconType } from 'react-icons'; import { BsQuestion } from 'react-icons/bs'; import { FiBox, FiEdit2, FiEyeOff, FiHeart, FiLoader, FiMic, FiPercent, FiStar, FiTrash2, FiUpload, FiX, } from 'react-icons/fi'; import { PiDotsSixThin } from 'react-icons/pi'; import type { IQuickAction } from '../../service/popup/popup.interface'; import TripleStarsIcon from '../icons/TripleStarsIcon'; import { DEFAULT_BLUE_RGB, DEFAULT_PINK_RGB, hexToRgb, } from '../../utils/colors'; import { DEFAULT_QUICK_ACTIONS } from '../../utils/quickAction'; const FALLBACK_PLACEHOLDERS = [ 'Search anything with AI...', 'Show me bestsellers', 'Find a gift for a friend', "What's trending this week?", 'Show popular items on sale', ]; const SLOT_ICONS: { icon: IconType; iconClassName: string }[] = [ { icon: BsQuestion, iconClassName: 'text-slate-800' }, { icon: FiStar, iconClassName: 'text-yellow-500' }, { icon: FiBox, iconClassName: 'text-blue-500' }, { icon: FiPercent, iconClassName: 'text-green-500' }, ]; function normalizeQuickActions(incoming?: IQuickAction[]): IQuickAction[] { // Merge overrides onto the default slots by index, mirroring // recomaze-custom-platform. Matching by key would break editing: the Label // field rewrites the key on every keystroke, so a key-based lookup loses the // override mid-edit and the input reverts to its default value. return DEFAULT_QUICK_ACTIONS.map((defaultQa, idx) => { const override = incoming?.[idx]; return override ? { ...defaultQa, ...override } : { ...defaultQa }; }); } interface AiShoppingAssistantProps { textColor?: string; buttonColor?: string; image: string | File | null; popupBorderColorLeft?: string; popupBorderColorRight?: string; headerMessage: string | null; expandedHeaderMessage: string | null; quickActions?: IQuickAction[]; onUpdateQuickAction?: (idx: number, patch: Partial) => void; onUploadQuickActionIcon?: (idx: number, file: File) => void; onSaveQuickActions?: (actions: IQuickAction[]) => Promise; isUploadingIcon?: boolean; placeholders?: string[]; } export const AiShoppingAssistantCard: FC = ({ textColor, image, popupBorderColorLeft, popupBorderColorRight, expandedHeaderMessage, headerMessage, quickActions, onUpdateQuickAction, onUploadQuickActionIcon, onSaveQuickActions, isUploadingIcon, placeholders, }) => { const [editingSlot, setEditingSlot] = useState(null); const leftRgb = hexToRgb(popupBorderColorLeft || ''); const rightRgb = hexToRgb(popupBorderColorRight || ''); const normalizedActions = useMemo( () => normalizeQuickActions(quickActions), [quickActions] ); const cardStyle: React.CSSProperties = { background: `linear-gradient( to bottom, rgba(${leftRgb}, 0.12) 0%, rgba(${leftRgb}, 0.12) 40%, rgba(${rightRgb}, 0.18) 100% )`, backdropFilter: 'blur(12px)', boxShadow: `0 25px 50px -12px rgba(${rightRgb}, 0.35)`, }; const editingAction = editingSlot !== null ? normalizedActions[editingSlot] : null; const editingDefaults = editingSlot !== null ? DEFAULT_QUICK_ACTIONS[editingSlot] : null; const editingIcon = editingSlot !== null ? SLOT_ICONS[editingSlot] : null; const editable = !!onUpdateQuickAction; return (
{/* Glass card */}
{/* Top chrome */}
{/* Logo / Sparkles */}
{image ? ( logo ) : ( )}

{headerMessage || ( Set your header message in Settings )}

{expandedHeaderMessage || ( Set your expanded message in Settings )}

{/* Quick actions */}
{normalizedActions.map((qa, idx) => { const visible = qa.visible ?? true; const label = qa.label || DEFAULT_QUICK_ACTIONS[idx].label; const customIcon = qa.icon_url; const slotIcon = SLOT_ICONS[idx]; const Icon = slotIcon.icon; if (!visible) { return editable ? ( ) : null; } return ( ); })}
{/* Search */}
{/* Edit Quick Action Modal */} {onUpdateQuickAction && editingSlot !== null && editingDefaults && editingIcon && ( setEditingSlot(null)} /> )}
); }; /* ─── Edit Quick Action Modal ─── */ interface EditModalProps { editingSlot: number; editingAction: IQuickAction | null; editingDefaults: IQuickAction; editingIcon: { icon: IconType; iconClassName: string }; isUploadingIcon?: boolean; onUpdateQuickAction: (idx: number, patch: Partial) => void; onUploadQuickActionIcon?: (idx: number, file: File) => void; onSaveQuickActions?: (actions: IQuickAction[]) => Promise; quickActions?: IQuickAction[]; onClose: () => void; } const EditQuickActionModal: FC = ({ editingSlot, editingAction, editingDefaults, editingIcon, isUploadingIcon, onUpdateQuickAction, onUploadQuickActionIcon, onSaveQuickActions, quickActions, onClose, }) => { const modalRef = useRef(null); const [isSaving, setIsSaving] = useState(false); const SlotIcon = editingIcon.icon; const handleDone = useCallback(async () => { if (onSaveQuickActions && quickActions) { setIsSaving(true); await onSaveQuickActions(quickActions); setIsSaving(false); } onClose(); }, [onSaveQuickActions, quickActions, onClose]); useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') handleDone(); }; document.addEventListener('keydown', handleEsc); return () => document.removeEventListener('keydown', handleEsc); }, [handleDone]); return (
{ if (e.target === e.currentTarget) handleDone(); }} >
{/* Header */}

Edit Quick Action

Customize this shortcut button on the greeting screen.

{/* Body */}
{/* Visibility toggle */}
{editingAction?.visible !== false ? 'Visible' : 'Hidden'}
{/* Label */}
onUpdateQuickAction(editingSlot, { label: e.target.value, key: e.target.value.toLowerCase().replace(/\s+/g, '_'), }) } placeholder="e.g. Summer Sale" maxLength={40} className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none hover:border-zinc-300 focus:border-zinc-400 focus:ring-1 focus:ring-zinc-200 transition-colors" />
{/* Query */}
onUpdateQuickAction(editingSlot, { query: e.target.value, }) } placeholder="e.g. summer sale products" maxLength={100} className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none hover:border-zinc-300 focus:border-zinc-400 focus:ring-1 focus:ring-zinc-200 transition-colors" />

Search query sent when clicked

{/* Icon upload */}
{editingAction?.icon_url ? (
Icon
) : (
)}
{isUploadingIcon && (

Uploading...

)}

PNG, JPG, SVG or WebP. Leave empty for default icon.

{/* Footer */}
); }; /* ─── Search Bar ─── */ const SearchBar: FC<{ popupBorderColorLeft?: string; popupBorderColorRight?: string; placeholders?: string[]; }> = ({ popupBorderColorLeft, popupBorderColorRight, placeholders }) => { const [value, setValue] = useState(''); const customStyles = { '--accent-pink-rgb': hexToRgb(popupBorderColorLeft || '') || DEFAULT_PINK_RGB, '--accent-blue-rgb': hexToRgb(popupBorderColorRight || '') || DEFAULT_BLUE_RGB, } as React.CSSProperties; const cleanedPlaceholders = useMemo(() => { const cleaned = (placeholders ?? []) .map(p => (typeof p === 'string' ? p.trim() : '')) .filter(p => p.length > 0); return cleaned.length > 0 ? cleaned : FALLBACK_PLACEHOLDERS; }, [placeholders]); return (
setValue(e.target.value)} />
); }; /* ─── Animated Placeholder ─── */ const AnimatedPlaceholder: FC<{ searchKey: string; placeholderList: string[]; }> = ({ searchKey, placeholderList }) => { const [text, setText] = useState(''); const [animateIndex, setAnimateIndex] = useState(0); const [charIndex, setCharIndex] = useState(0); const [forward, setForward] = useState(true); const mainTimeoutRef = useRef | null>(null); const nestedTimeoutRef = useRef | null>(null); useEffect(() => { setAnimateIndex(0); setCharIndex(0); setForward(true); setText(''); }, [placeholderList]); useEffect(() => { if (searchKey !== '') return; if (!placeholderList || placeholderList.length === 0) return; const safeIndex = animateIndex % placeholderList.length; const currentText = placeholderList[safeIndex]; if (!currentText) return; const isTyping = forward && charIndex <= currentText.length; const isDeleting = !forward && charIndex >= 0; const minSpeed = 70; const maxSpeed = 140; const speed = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed; mainTimeoutRef.current = setTimeout( () => { if (forward) { if (charIndex <= currentText.length) { setText(currentText.slice(0, charIndex)); setCharIndex(charIndex + 1); } else { nestedTimeoutRef.current = setTimeout( () => setForward(false), 1000 ); } } else if (charIndex > 0) { setText(currentText.slice(0, charIndex)); setCharIndex(charIndex - 1); } else { setText(''); setCharIndex(0); setForward(true); setAnimateIndex(prev => (prev + 1) % placeholderList.length); } }, isTyping || isDeleting ? speed : 1000 ); return () => { if (mainTimeoutRef.current) clearTimeout(mainTimeoutRef.current); if (nestedTimeoutRef.current) clearTimeout(nestedTimeoutRef.current); }; }, [charIndex, forward, animateIndex, placeholderList, searchKey]); if (searchKey !== '') return null; return ( {Array.from(text).map((char, i, arr) => { const isLast = i === arr.length - 1; return ( {char} ); })} | ); };