@paramms/chat-widget 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sources":["../src/react.tsx"],"sourcesContent":["// react.tsx — React wrapper around mount().\n//\n// Next.js App Router: add 'use client' to whichever file imports these.\n// The widget needs WebSocket + DOM — it cannot render on the server.\n//\n// Import path: '@paramms/chat-widget/react'\n//\n// Two components:\n// ChatWidget — general support chat (hotel, SaaS, helpdesk, etc.)\n// MarketplaceChat — one thread per listing (used cars, rentals, etc.)\n//\n// Both: only url + profileId are required. userId is always optional —\n// anonymous guests are handled automatically via localStorage.\n\nimport { useEffect, useRef } from 'react'\nimport { mount, type MountOptions, type WidgetHandle } from './index.js'\n\n// ── Shared base props ─────────────────────────────────────────────────────────\n\ninterface BaseProps {\n /** Relay WebSocket URL — set as NEXT_PUBLIC_RELAY_WS_URL in .env — required */\n url: string\n /** Your Relay profile ID — set as NEXT_PUBLIC_RELAY_PROFILE_ID in .env — required */\n profileId: string\n\n /** Your logged-in user's stable ID. When omitted the widget automatically\n * assigns a persistent anonymous ID from localStorage — no login required. */\n userId?: string\n /** Shown to agents instead of the raw user ID */\n userName?: string\n /** Shown to agents so they can follow up by email */\n userEmail?: string\n /** Avatar URL shown in the widget header and dashboard */\n userAvatar?: string\n\n /** Brand colour hex, e.g. \"#1a56db\". Defaults to the profile theme colour. */\n accent?: string\n /** Render as a floating launcher button instead of inline */\n launcher?: boolean\n /** Launcher position — default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left'\n /** Pre-set reply chips shown above the input */\n quickReplies?: string[]\n /** Container height when rendered inline. Default: '100%' */\n height?: string\n /** i18n string overrides for non-English sites */\n i18n?: MountOptions['i18n']\n /** ISO language code for auto-translating incoming messages */\n translateLang?: string\n}\n\n// ── ChatWidget ────────────────────────────────────────────────────────────────\n\nexport interface ChatWidgetProps extends BaseProps {\n /** Optional context card shown at the top of the chat\n * (e.g. the support ticket, booking, or order being discussed) */\n contextTitle?: string\n contextSubtitle?: string\n contextStatus?: string\n\n /** Show the WhatsApp-style thread list as the home screen.\n * Use when users can have more than one thread on this profile\n * (e.g. one thread per support ticket, one per booking). */\n showChatList?: boolean\n\n /** Stable item ID — pins this conversation to a specific item/thread.\n * When showChatList is also true, the widget opens this thread immediately\n * but the user can navigate back to the full list. */\n subjectId?: string\n}\n\n/**\n * General-purpose support chat widget. Only `url` and `profileId` are required.\n * All other props are optional — anonymous guests work without any configuration.\n *\n * @example Basic support chat\n * ```tsx\n * <ChatWidget\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * userName={session?.user.name}\n * userEmail={session?.user.email}\n * />\n * ```\n *\n * @example Multi-thread (tickets, bookings, orders)\n * ```tsx\n * <ChatWidget\n * url={...} profileId={...}\n * showChatList\n * subjectId={`ticket_${ticket.id}`}\n * contextTitle={ticket.title}\n * contextStatus={ticket.status}\n * userId={session?.user.id}\n * />\n * ```\n */\nexport function ChatWidget({\n url, profileId,\n userId, userName, userEmail, userAvatar,\n contextTitle, contextSubtitle, contextStatus,\n showChatList, subjectId,\n accent, launcher, position, quickReplies, height = '100%',\n i18n, translateLang,\n}: ChatWidgetProps): JSX.Element {\n const ref = useRef<HTMLDivElement>(null)\n const handleRef = useRef<WidgetHandle | null>(null)\n\n useEffect(() => {\n if (!ref.current) return\n handleRef.current = mount({\n el: ref.current,\n url,\n profileId,\n ...(subjectId ? { subjectId } : {}),\n ...(userId ? { token: userId } : {}),\n ...(userId || userName || userEmail || userAvatar ? {\n user: {\n ...(userName ? { name: userName } : {}),\n ...(userEmail ? { email: userEmail } : {}),\n ...(userAvatar ? { avatar: userAvatar } : {}),\n },\n } : {}),\n ...(contextTitle ? {\n subject: {\n title: contextTitle,\n ...(contextSubtitle ? { subtitle: contextSubtitle } : {}),\n ...(contextStatus ? { status: contextStatus } : {}),\n },\n } : {}),\n ...(showChatList ? { showChatList: true } : {}),\n ...(quickReplies ? { quickReplies } : {}),\n ...(accent ? { accent } : {}),\n ...(translateLang ? { translateLang } : {}),\n ...(i18n ? { i18n } : {}),\n ...(launcher ? { launcher, position: position ?? 'bottom-right' } : {}),\n })\n return () => { handleRef.current?.close(); handleRef.current = null }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, profileId, subjectId, userId])\n\n // When launcher=true, still render the div — mount() moves it inside the\n // fixed panel above the bubble. Zero size so it doesn't affect page layout.\n return (\n <div\n ref={ref}\n style={launcher ? { width: 0, height: 0, overflow: 'hidden' } : { width: '100%', height, minHeight: height === '100%' ? '480px' : undefined }}\n />\n )\n}\n\n// ── MarketplaceChat ───────────────────────────────────────────────────────────\n\nexport interface MarketplaceChatProps extends BaseProps {\n /** Unique ID for this listing — each listing gets its own thread.\n * Omit for a general (non-item-specific) conversation. */\n listingId?: string\n /** Shown at the top of the chat — e.g. \"2019 Toyota Camry SE\" */\n listingTitle?: string\n /** One-line detail — e.g. \"45,000 km · Automatic\" */\n listingMeta?: string\n /** Price shown as a tag — e.g. 12500 → \"$12,500\" */\n listingPrice?: number\n /** Status badge — e.g. \"Available\", \"Sold\", \"Pending\" */\n listingStatus?: string\n}\n\nconst DEFAULT_MARKETPLACE_REPLIES = [\n 'Is this still available?',\n 'Can I schedule a viewing?',\n 'What is your best price?',\n]\n\n/**\n * Marketplace chat widget. Two modes depending on whether `listingId` is provided:\n *\n * **With `listingId` (listing page)** — opens that listing's chat immediately.\n * No list screen. The buyer is already looking at the item so context is clear.\n * ```tsx\n * // On your listing detail page:\n * <MarketplaceChat\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * listingId={car.id}\n * listingTitle={car.title}\n * listingPrice={car.price}\n * listingMeta={`${car.mileage.toLocaleString()} km · ${car.gearbox}`}\n * listingStatus=\"Available\"\n * userId={session?.user.id}\n * userName={session?.user.name}\n * userEmail={session?.user.email}\n * />\n * ```\n *\n * **Without `listingId` (My Messages / inbox page)** — shows the WhatsApp-style\n * thread list as the home screen. No conversation is opened until the user taps one.\n * Use this on a dedicated `/messages` or `/inbox` page.\n * ```tsx\n * // On your /messages page:\n * <MarketplaceChat\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * userName={session?.user.name}\n * />\n * ```\n *\n * Only `url` and `profileId` are required. All other props are optional.\n */\nexport function MarketplaceChat({\n url, profileId,\n listingId, listingTitle, listingMeta, listingPrice, listingStatus,\n userId, userName, userEmail, userAvatar,\n accent, launcher, position, quickReplies, height = '100%',\n i18n, translateLang,\n}: MarketplaceChatProps): JSX.Element {\n const ref = useRef<HTMLDivElement>(null)\n const handleRef = useRef<WidgetHandle | null>(null)\n\n useEffect(() => {\n if (!ref.current) return\n handleRef.current = mount({\n el: ref.current,\n url,\n profileId,\n\n // ── Listing context ────────────────────────────────────────────────────\n // When listingId is present: open that listing's chat directly (no list).\n // When listingId is absent: show the thread list (dedicated inbox page).\n ...(listingId ? { subjectId: `listing_${listingId}` } : {}),\n\n ...(userId ? { token: userId } : {}),\n ...(userId || userName || userEmail || userAvatar ? {\n user: {\n ...(userName ? { name: userName } : {}),\n ...(userEmail ? { email: userEmail } : {}),\n ...(userAvatar ? { avatar: userAvatar } : {}),\n ...(listingId ? { meta: { listingId } } : {}),\n },\n } : {}),\n\n ...(listingTitle ? {\n subject: {\n title: listingTitle,\n ...(listingMeta ? { subtitle: listingMeta } : {}),\n ...(listingPrice ? { tags: [`$${listingPrice.toLocaleString()}`] } : {}),\n ...(listingStatus ? { status: listingStatus } : {}),\n },\n } : {}),\n\n // showChatList is true in both cases but behaves differently:\n // • With subjectId → chat screen shown immediately on open (in index.ts)\n // • Without subjectId → list screen is home (the dedicated inbox case)\n showChatList: true,\n quickReplies: quickReplies ?? DEFAULT_MARKETPLACE_REPLIES,\n ...(accent ? { accent } : {}),\n ...(translateLang ? { translateLang } : {}),\n ...(i18n ? { i18n } : {}),\n ...(launcher ? { launcher, position: position ?? 'bottom-right' } : {}),\n })\n return () => { handleRef.current?.close(); handleRef.current = null }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, profileId, listingId, userId])\n\n // When launcher=true, still render the div — mount() moves it inside the\n // fixed panel above the bubble. Zero size so it doesn't affect page layout.\n return (\n <div\n ref={ref}\n style={launcher ? { width: 0, height: 0, overflow: 'hidden' } : { width: '100%', height, minHeight: height === '100%' ? '480px' : undefined }}\n />\n )\n}\n"],"names":["ChatWidget","url","profileId","userId","userName","userEmail","userAvatar","contextTitle","contextSubtitle","contextStatus","showChatList","subjectId","accent","launcher","position","quickReplies","height","i18n","translateLang","ref","useRef","handleRef","useEffect","mount","_a","jsx","DEFAULT_MARKETPLACE_REPLIES","MarketplaceChat","listingId","listingTitle","listingMeta","listingPrice","listingStatus"],"mappings":"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"}
1
+ {"version":3,"file":"react.js","sources":["../src/react.tsx"],"sourcesContent":["// react.tsx — React wrapper around mount().\n//\n// Next.js App Router: add 'use client' to whichever file imports these.\n// The widget needs WebSocket + DOM — it cannot render on the server.\n//\n// Import path: '@paramms/chat-widget/react'\n//\n// Two components:\n// ChatWidget — general support chat (hotel, SaaS, helpdesk, etc.)\n// MarketplaceChat — one thread per listing (used cars, rentals, etc.)\n//\n// Both: only url + profileId are required. userId is always optional —\n// anonymous guests are handled automatically via localStorage.\n\nimport { useEffect, useRef, useState } from 'react'\nimport { mount, type MountOptions, type WidgetHandle } from './index.js'\n\n// ── Shared base props ─────────────────────────────────────────────────────────\n\ninterface BaseProps {\n /** Relay WebSocket URL — set as NEXT_PUBLIC_RELAY_WS_URL in .env — required */\n url: string\n /** Your Relay profile ID — set as NEXT_PUBLIC_RELAY_PROFILE_ID in .env — required */\n profileId: string\n\n /** Your logged-in user's stable ID. When omitted the widget automatically\n * assigns a persistent anonymous ID from localStorage — no login required. */\n userId?: string\n /** Shown to agents instead of the raw user ID */\n userName?: string\n /** Shown to agents so they can follow up by email */\n userEmail?: string\n /** Avatar URL shown in the widget header and dashboard */\n userAvatar?: string\n\n /** Brand colour hex, e.g. \"#1a56db\". Defaults to the profile theme colour. */\n accent?: string\n /** Render as a floating launcher button instead of inline */\n launcher?: boolean\n /** Launcher position — default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left'\n /** Pre-set reply chips shown above the input */\n quickReplies?: string[]\n /** Container height when rendered inline. Default: '100%' */\n height?: string\n /** i18n string overrides for non-English sites */\n i18n?: MountOptions['i18n']\n /** ISO language code for auto-translating incoming messages */\n translateLang?: string\n}\n\n// ── ChatWidget ────────────────────────────────────────────────────────────────\n\nexport interface ChatWidgetProps extends BaseProps {\n /** Optional context card shown at the top of the chat\n * (e.g. the support ticket, booking, or order being discussed) */\n contextTitle?: string\n contextSubtitle?: string\n contextStatus?: string\n\n /** Stable item ID — pins this conversation to a specific item/thread. */\n subjectId?: string\n}\n\n/**\n * General-purpose support chat widget. Only `url` and `profileId` are required.\n * All other props are optional — anonymous guests work without any configuration.\n *\n * @example Basic support chat\n * ```tsx\n * <ChatWidget\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * userName={session?.user.name}\n * userEmail={session?.user.email}\n * />\n * ```\n *\n * @example Multi-thread (tickets, bookings, orders)\n * ```tsx\n * <ChatWidget\n * url={...} profileId={...}\n * showChatList\n * subjectId={`ticket_${ticket.id}`}\n * contextTitle={ticket.title}\n * contextStatus={ticket.status}\n * userId={session?.user.id}\n * />\n * ```\n */\nexport function ChatWidget({\n url, profileId,\n userId, userName, userEmail, userAvatar,\n contextTitle, contextSubtitle, contextStatus,\n subjectId,\n accent, launcher, position, quickReplies, height = '100%',\n i18n, translateLang,\n}: ChatWidgetProps): JSX.Element {\n const ref = useRef<HTMLDivElement>(null)\n const handleRef = useRef<WidgetHandle | null>(null)\n\n useEffect(() => {\n if (!ref.current) return\n handleRef.current = mount({\n el: ref.current,\n url,\n profileId,\n ...(subjectId ? { subjectId } : {}),\n ...(userId ? { token: userId } : {}),\n ...(userId || userName || userEmail || userAvatar ? {\n user: {\n ...(userName ? { name: userName } : {}),\n ...(userEmail ? { email: userEmail } : {}),\n ...(userAvatar ? { avatar: userAvatar } : {}),\n },\n } : {}),\n ...(contextTitle ? {\n subject: {\n title: contextTitle,\n ...(contextSubtitle ? { subtitle: contextSubtitle } : {}),\n ...(contextStatus ? { status: contextStatus } : {}),\n },\n } : {}),\n ...(quickReplies ? { quickReplies } : {}),\n ...(accent ? { accent } : {}),\n ...(translateLang ? { translateLang } : {}),\n ...(i18n ? { i18n } : {}),\n ...(launcher ? { launcher, position: position ?? 'bottom-right' } : {}),\n })\n return () => { handleRef.current?.close(); handleRef.current = null }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, profileId, subjectId, userId])\n\n // When launcher=true, still render the div — mount() moves it inside the\n // fixed panel above the bubble. Zero size so it doesn't affect page layout.\n return (\n <div\n ref={ref}\n style={launcher ? { width: 0, height: 0, overflow: 'hidden' } : { width: '100%', height, minHeight: height === '100%' ? '480px' : undefined }}\n />\n )\n}\n\n// ── MarketplaceChat ───────────────────────────────────────────────────────────\n\nexport interface MarketplaceChatProps extends BaseProps {\n /** Unique ID for this listing — each listing gets its own thread.\n * Omit for a general (non-item-specific) conversation. */\n listingId?: string\n /** Shown at the top of the chat — e.g. \"2019 Toyota Camry SE\" */\n listingTitle?: string\n /** One-line detail — e.g. \"45,000 km · Automatic\" */\n listingMeta?: string\n /** Price shown as a tag — e.g. 12500 → \"$12,500\" */\n listingPrice?: number\n /** Status badge — e.g. \"Available\", \"Sold\", \"Pending\" */\n listingStatus?: string\n}\n\nconst DEFAULT_MARKETPLACE_REPLIES = [\n 'Is this still available?',\n 'Can I schedule a viewing?',\n 'What is your best price?',\n]\n\n/**\n * Marketplace chat widget. Two modes depending on whether `listingId` is provided:\n *\n * **With `listingId` (listing page)** — opens that listing's chat immediately.\n * No list screen. The buyer is already looking at the item so context is clear.\n * ```tsx\n * // On your listing detail page:\n * <MarketplaceChat\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * listingId={car.id}\n * listingTitle={car.title}\n * listingPrice={car.price}\n * listingMeta={`${car.mileage.toLocaleString()} km · ${car.gearbox}`}\n * listingStatus=\"Available\"\n * userId={session?.user.id}\n * userName={session?.user.name}\n * userEmail={session?.user.email}\n * />\n * ```\n *\n * **Without `listingId` (My Messages / inbox page)** — shows the WhatsApp-style\n * thread list as the home screen. No conversation is opened until the user taps one.\n * Use this on a dedicated `/messages` or `/inbox` page.\n * ```tsx\n * // On your /messages page:\n * <MarketplaceChat\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * userName={session?.user.name}\n * />\n * ```\n *\n * Only `url` and `profileId` are required. All other props are optional.\n */\nexport function MarketplaceChat({\n url, profileId,\n listingId, listingTitle, listingMeta, listingPrice, listingStatus,\n userId, userName, userEmail, userAvatar,\n accent, launcher, position, quickReplies, height = '100%',\n i18n, translateLang,\n}: MarketplaceChatProps): JSX.Element {\n const ref = useRef<HTMLDivElement>(null)\n const handleRef = useRef<WidgetHandle | null>(null)\n\n useEffect(() => {\n if (!ref.current) return\n handleRef.current = mount({\n el: ref.current,\n url,\n profileId,\n\n // ── Listing context ────────────────────────────────────────────────────\n // When listingId is present: open that listing's chat directly (no list).\n // When listingId is absent: show the thread list (dedicated inbox page).\n ...(listingId ? { subjectId: `listing_${listingId}` } : {}),\n\n ...(userId ? { token: userId } : {}),\n ...(userId || userName || userEmail || userAvatar ? {\n user: {\n ...(userName ? { name: userName } : {}),\n ...(userEmail ? { email: userEmail } : {}),\n ...(userAvatar ? { avatar: userAvatar } : {}),\n ...(listingId ? { meta: { listingId } } : {}),\n },\n } : {}),\n\n ...(listingTitle ? {\n subject: {\n title: listingTitle,\n ...(listingMeta ? { subtitle: listingMeta } : {}),\n ...(listingPrice ? { tags: [`$${listingPrice.toLocaleString()}`] } : {}),\n ...(listingStatus ? { status: listingStatus } : {}),\n },\n } : {}),\n\n\n quickReplies: quickReplies ?? DEFAULT_MARKETPLACE_REPLIES,\n ...(accent ? { accent } : {}),\n ...(translateLang ? { translateLang } : {}),\n ...(i18n ? { i18n } : {}),\n ...(launcher ? { launcher, position: position ?? 'bottom-right' } : {}),\n })\n return () => { handleRef.current?.close(); handleRef.current = null }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, profileId, listingId, userId])\n\n // When launcher=true, still render the div — mount() moves it inside the\n // fixed panel above the bubble. Zero size so it doesn't affect page layout.\n return (\n <div\n ref={ref}\n style={launcher ? { width: 0, height: 0, overflow: 'hidden' } : { width: '100%', height, minHeight: height === '100%' ? '480px' : undefined }}\n />\n )\n}\n\n// ── ChatList ──────────────────────────────────────────────────────────────────\n\nexport interface ChatAppProps {\n /** Relay WebSocket URL — required */\n url: string\n /** Your Relay profile ID — required */\n profileId: string\n /** Your logged-in user's stable ID. Omit for anonymous (uses localStorage UID) */\n userId?: string\n /** Optional: shown to agents in the dashboard */\n userName?: string\n /** Optional: shown to agents in the dashboard */\n userEmail?: string\n /** Brand colour hex — default '#f5713c' */\n accent?: string\n /** Container height. Default: '100%' */\n height?: string\n /** i18n overrides */\n i18n?: import('./chatlist.js').ChatListOptions['i18n']\n}\n\n/**\n * Full chat-app component: conversation list on the left (or full screen on\n * mobile), chat room on the right when a thread is selected.\n *\n * Works like a standalone messaging app — tapping a thread opens the chat\n * inline without navigating away from the page.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { ChatList } from '@paramms/chat-widget/react'\n *\n * export default function MessagesPage({ session }) {\n * return (\n * <div style={{ height: '100vh' }}>\n * <ChatList\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * userName={session?.user.name}\n * userEmail={session?.user.email}\n * />\n * </div>\n * )\n * }\n * ```\n */\nexport function ChatApp({\n url, profileId, userId, userName, userEmail, accent, height = '100%', i18n,\n}: ChatAppProps): JSX.Element {\n type Entry = import('./chatlist.js').ChatListEntry\n const [selected, setSelected] = useState<Entry | null>(null)\n const listRef = useRef<HTMLDivElement>(null)\n const chatRef = useRef<HTMLDivElement>(null)\n const handleRef = useRef<import('./chatlist.js').ChatListHandle | null>(null)\n const chatHandle = useRef<WidgetHandle | null>(null)\n\n // Build the chat list\n useEffect(() => {\n if (!listRef.current) return\n import('./chatlist.js').then(({ mountChatList }) => {\n handleRef.current = mountChatList({\n el: listRef.current!,\n url,\n profileId,\n onSelect: (entry) => setSelected(entry),\n ...(userId ? { userId } : {}),\n ...(accent ? { accent } : {}),\n ...(i18n ? { i18n } : {}),\n })\n })\n return () => { handleRef.current?.close(); handleRef.current = null }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [url, profileId, userId])\n\n // Open a ChatWidget when a thread is selected\n useEffect(() => {\n if (!chatRef.current || !selected) return\n chatHandle.current?.close()\n chatHandle.current = mount({\n el: chatRef.current,\n url,\n profileId,\n ...(selected.subjectId ? { subjectId: selected.subjectId } : {}),\n ...(userId ? { token: userId } : {}),\n ...(userId || userName || userEmail ? {\n user: {\n ...(userName ? { name: userName } : {}),\n ...(userEmail ? { email: userEmail } : {}),\n },\n } : {}),\n ...(selected.subjectTitle ? {\n subject: {\n title: selected.subjectTitle,\n ...(selected.subjectMeta ? { subtitle: selected.subjectMeta } : {}),\n },\n } : {}),\n ...(accent ? { accent } : {}),\n })\n return () => { chatHandle.current?.close(); chatHandle.current = null }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selected?.id])\n\n const isMobile = typeof window !== 'undefined' && window.innerWidth < 768\n\n return (\n <div style={{ display: 'flex', width: '100%', height, minHeight: '400px', overflow: 'hidden' }}>\n {/* List panel — hidden on mobile when a chat is open */}\n <div style={{\n width: isMobile ? '100%' : '360px',\n flex: isMobile && selected ? 'none' : undefined,\n display: isMobile && selected ? 'none' : 'flex',\n flexDirection: 'column',\n borderRight: isMobile ? 'none' : '1px solid #e8eaed',\n minWidth: 0,\n }}>\n <div ref={listRef} style={{ width: '100%', height: '100%' }} />\n </div>\n\n {/* Chat panel — shows when a thread is selected */}\n {selected ? (\n <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, position: 'relative' }}>\n {/* Back button on mobile */}\n {isMobile && (\n <button\n onClick={() => setSelected(null)}\n style={{ position: 'absolute', top: 12, left: 12, zIndex: 10, background: 'none', border: 'none', fontSize: 22, cursor: 'pointer', color: '#1c1b1a' }}\n >\n ←\n </button>\n )}\n <div ref={chatRef} style={{ width: '100%', height: '100%' }} />\n </div>\n ) : (\n <div style={{ flex: 1, display: isMobile ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center', color: '#9b9690', fontSize: 14 }}>\n Select a conversation\n </div>\n )}\n </div>\n )\n}\n\n// ── ChatAppLauncher ───────────────────────────────────────────────────────────\n\nexport interface ChatAppLauncherProps {\n /** Relay WebSocket URL — required */\n url: string\n /** Your Relay profile ID — required */\n profileId: string\n /** Your logged-in user's stable ID */\n userId?: string\n userName?: string\n userEmail?: string\n /** Brand colour hex — default '#4F63F5' */\n accent?: string\n\n /** true → floating bubble fixed to the corner of the screen (like Intercom)\n * false → an inline \"Messages\" button that expands a panel below it\n * Default: true */\n floating?: boolean\n /** Only used when floating=true. Default: 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left'\n /** Label shown on the button. Default: '💬' for floating, 'Messages' for inline */\n label?: string\n /** Width of the chat panel. Default: '420px' (floating) or '100%' (inline) */\n panelWidth?: string\n /** Height of the chat panel. Default: '600px' */\n panelHeight?: string\n}\n\n/**\n * A button that opens the full ChatApp (list → chat) in a panel.\n *\n * floating=true → fixed bubble in the corner of the screen, like WhatsApp Web's\n * chat button or Intercom.\n * floating=false → an inline button (e.g. in your nav bar) that expands a panel\n * below it.\n *\n * @example Floating bubble (bottom-right)\n * ```tsx\n * <ChatAppLauncher\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * floating\n * position=\"bottom-right\"\n * />\n * ```\n *\n * @example Inline nav button\n * ```tsx\n * <ChatAppLauncher\n * url={process.env.NEXT_PUBLIC_RELAY_WS_URL}\n * profileId={process.env.NEXT_PUBLIC_RELAY_PROFILE_ID}\n * userId={session?.user.id}\n * floating={false}\n * label=\"Messages\"\n * />\n * ```\n */\nexport function ChatAppLauncher({\n url, profileId, userId, userName, userEmail,\n accent = '#4F63F5',\n floating = true,\n position = 'bottom-right',\n label,\n panelWidth,\n panelHeight = '600px',\n}: ChatAppLauncherProps): JSX.Element {\n const [open, setOpen] = useState(false)\n const btnRef = useRef<HTMLButtonElement>(null)\n\n const isRight = position === 'bottom-right'\n const defaultLabel = floating ? '💬' : 'Messages'\n const btnLabel = label ?? defaultLabel\n\n // Close on Escape\n useEffect(() => {\n const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [])\n\n // ── Floating mode ──────────────────────────────────────────────────────────\n if (floating) {\n const isMobile = typeof window !== 'undefined' && window.innerWidth < 480\n const pWidth = isMobile ? '100vw' : (panelWidth ?? '420px')\n const pHeight = isMobile ? '100dvh' : panelHeight\n\n return (\n <div style={{\n position: 'fixed',\n [isRight ? 'right' : 'left']: '20px',\n bottom: '20px',\n zIndex: 9999,\n display: 'flex',\n flexDirection: 'column',\n alignItems: isRight ? 'flex-end' : 'flex-start',\n gap: '12px',\n }}>\n {/* Panel */}\n <div style={{\n width: pWidth,\n height: pHeight,\n borderRadius: isMobile ? '0' : '16px',\n overflow: 'hidden',\n boxShadow: '0 8px 40px rgba(0,0,0,.18)',\n background: '#fff',\n display: open ? 'flex' : 'none',\n flexDirection: 'column',\n transformOrigin: `bottom ${isRight ? 'right' : 'left'}`,\n animation: open ? 'ocl-pop-in .18s ease' : 'none',\n }}>\n <ChatApp\n url={url} profileId={profileId}\n {...(userId ? { userId } : {})}\n {...(userName ? { userName } : {})}\n {...(userEmail ? { userEmail } : {})}\n accent={accent} height=\"100%\"\n />\n </div>\n\n {/* Bubble button */}\n <button\n ref={btnRef}\n onClick={() => setOpen(o => !o)}\n style={{\n width: '56px', height: '56px', borderRadius: '50%',\n background: accent, color: '#fff', border: 'none',\n fontSize: '24px', cursor: 'pointer', flexShrink: 0,\n boxShadow: '0 4px 16px rgba(0,0,0,.25)',\n transition: 'transform .15s',\n }}\n onMouseEnter={e => { (e.target as HTMLElement).style.transform = 'scale(1.08)' }}\n onMouseLeave={e => { (e.target as HTMLElement).style.transform = 'scale(1)' }}\n >\n {open ? '✕' : btnLabel}\n </button>\n\n <style>{`@keyframes ocl-pop-in { from { opacity:0; transform:scale(.95) } to { opacity:1; transform:scale(1) } }`}</style>\n </div>\n )\n }\n\n // ── Inline mode ────────────────────────────────────────────────────────────\n const pWidth = panelWidth ?? '420px'\n\n return (\n <div style={{ position: 'relative', display: 'inline-block' }}>\n {/* Trigger button */}\n <button\n ref={btnRef}\n onClick={() => setOpen(o => !o)}\n style={{\n display: 'inline-flex', alignItems: 'center', gap: '6px',\n padding: '8px 16px', borderRadius: '8px',\n background: open ? accent : 'transparent',\n color: open ? '#fff' : accent,\n border: `2px solid ${accent}`,\n fontSize: '14px', fontWeight: 600, cursor: 'pointer',\n transition: 'background .15s, color .15s',\n }}\n >\n 💬 {btnLabel}\n </button>\n\n {/* Dropdown panel */}\n {open && (\n <>\n {/* Backdrop — click outside to close */}\n <div\n onClick={() => setOpen(false)}\n style={{ position: 'fixed', inset: 0, zIndex: 999 }}\n />\n <div style={{\n position: 'absolute',\n top: 'calc(100% + 8px)',\n right: 0,\n width: pWidth,\n height: panelHeight,\n borderRadius: '16px',\n overflow: 'hidden',\n boxShadow: '0 8px 40px rgba(0,0,0,.18)',\n background: '#fff',\n zIndex: 1000,\n animation: 'ocl-pop-in .18s ease',\n }}>\n <ChatApp\n url={url} profileId={profileId}\n {...(userId ? { userId } : {})}\n {...(userName ? { userName } : {})}\n {...(userEmail ? { userEmail } : {})}\n accent={accent} height=\"100%\"\n />\n </div>\n <style>{`@keyframes ocl-pop-in { from { opacity:0; transform:translateY(-8px) } to { opacity:1; transform:translateY(0) } }`}</style>\n </>\n )}\n </div>\n )\n}\n"],"names":["ChatWidget","url","profileId","userId","userName","userEmail","userAvatar","contextTitle","contextSubtitle","contextStatus","subjectId","accent","launcher","position","quickReplies","height","i18n","translateLang","ref","useRef","handleRef","useEffect","mount","_a","jsx","DEFAULT_MARKETPLACE_REPLIES","MarketplaceChat","listingId","listingTitle","listingMeta","listingPrice","listingStatus","ChatApp","selected","setSelected","useState","listRef","chatRef","chatHandle","mountChatList","entry","isMobile","jsxs","ChatAppLauncher","floating","label","panelWidth","panelHeight","open","setOpen","btnRef","isRight","btnLabel","handler","e","pWidth","pHeight","o","Fragment"],"mappings":"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"}
@@ -22,10 +22,6 @@ export interface WidgetConfig {
22
22
  offline?: string;
23
23
  poweredBy?: string;
24
24
  };
25
- /** When showChatList=true, start on the chat screen instead of the list screen.
26
- * Set when a specific conversation/listing is already known (e.g. MarketplaceChat
27
- * with listingId). The list is still accessible via the back button. */
28
- startOnChat?: boolean;
29
25
  }
30
26
  export interface RendererHandlers {
31
27
  onSend(text: string): void;
@@ -45,24 +41,6 @@ export interface RendererHandlers {
45
41
  /** Translate a message's text for display. Return null if unavailable —
46
42
  * the renderer shows a brief "unavailable" hint and leaves the original. */
47
43
  onTranslate?(text: string): Promise<string | null>;
48
- /** Multi-subject chat list (e.g. a marketplace with one thread per item):
49
- * fetch the guest's other conversations. Omit to hide the list button
50
- * entirely — single-conversation embeds don't need this. */
51
- onListChats?(): Promise<ChatListEntry[]>;
52
- /** Switch the active conversation to a different one from the list —
53
- * effectively a re-open with a different subjectId. */
54
- onSwitchChat?(entry: ChatListEntry): void;
55
- }
56
- export interface ChatListEntry {
57
- id: string;
58
- subjectId?: string;
59
- subjectTitle?: string;
60
- state: string;
61
- updatedAt: number;
62
- /** Server-assigned highest message seq — used to compute unread count. */
63
- lastSeq?: number;
64
- /** Snippet of the last message text — shown as preview in the list row. */
65
- lastMessage?: string;
66
44
  }
67
45
  /** Renders a ChatStore into a host element in the Image-1 layout: header →
68
46
  * subject card → action chips → chat → quick replies → input. Self-injects its
@@ -104,38 +82,15 @@ export declare class Renderer {
104
82
  setScrollCleanup(fn: () => void): void;
105
83
  private readonly translationCache;
106
84
  private readonly showingTranslation;
107
- private listScreen;
108
- private chatScreen;
109
- private listBody;
110
- private listSearch;
111
- private backBtn;
112
- private activeChatId;
113
- private inChatScreen;
114
- private allEntries;
115
85
  /** Last seq the guest has seen per conversationId — used to compute unread badges. */
116
- private readonly seenSeq;
117
- private seenSeqKey;
118
- /** Load seenSeq from localStorage (keyed by userId+profileId). */
119
- initSeenSeq(key: string): void;
120
- setSeenSeq(conversationId: string, seq: number): void;
121
86
  constructor(root: HTMLElement, me: string, h: RendererHandlers, cfg?: WidgetConfig);
122
- /** Fetch the list and re-render it. Called on mount and after switching chats. */
123
- private destroyed;
124
- /** Call when the widget is unmounted. Disconnects scroll listeners and
125
- * prevents in-flight fetches from writing to dead DOM. */
87
+ /** Call when the widget is unmounted. Disconnects scroll listeners. */
126
88
  destroy(): void;
127
- refreshList(): void;
128
89
  /** Render list rows, optionally filtered by search query. */
129
- private renderListRows;
130
- private isUnread;
131
90
  /** Build a single WhatsApp-style conversation row. */
132
- private buildRow;
133
91
  /** Returns true when the chat screen is visible (not the list). */
134
- isInChatScreen(): boolean;
135
92
  /** Navigate to the chat screen (slide list left, slide chat in from right). */
136
- showChatScreen(): void;
137
93
  /** Navigate back to the list screen. */
138
- showListScreen(): void;
139
94
  private flushSend;
140
95
  private signalTyping;
141
96
  render(store: ChatStore): void;
package/dist/uid.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** Persistent anonymous identity, reused across reloads (per the old widget's
2
+ * oc_uid pattern). */
3
+ export declare function persistentUid(): string;
package/dist/uid.js ADDED
@@ -0,0 +1,63 @@
1
+ function y(t) {
2
+ return t.replace(/^ws/, "http").replace(/\/ws\/?$/, "");
3
+ }
4
+ function m(t, a, e, s) {
5
+ const r = `beforeSeq=${e}&limit=${s}`;
6
+ return [
7
+ `${t}/conversations/${a}/messages?${r}`,
8
+ `${t}/conversations/${a}/history?${r}`
9
+ ];
10
+ }
11
+ async function g(t, a, e, s, r = 20) {
12
+ for (const l of m(t, a, s, r))
13
+ try {
14
+ const o = await fetch(l, { headers: { authorization: `Bearer ${e}` } });
15
+ if (!o.ok) continue;
16
+ const n = await o.json();
17
+ return { messages: n.messages ?? [], hasMore: n.hasMore ?? !1 };
18
+ } catch {
19
+ }
20
+ return null;
21
+ }
22
+ async function M(t, a, e, s, r) {
23
+ const l = y(t), o = await g(l, e, a, Number.MAX_SAFE_INTEGER);
24
+ if (!o || (o.messages.length ? (s.apply({ type: "sync", conversationId: e, messages: o.messages }), s.apply({ type: "history", conversationId: e, messages: [], hasMore: o.hasMore }), r.render(s)) : s.apply({ type: "history", conversationId: e, messages: [], hasMore: !1 }), !o.hasMore)) return;
25
+ let n = !1;
26
+ const p = async () => {
27
+ if (n || !s.hasMoreHistory) return;
28
+ n = !0;
29
+ const h = s.messages()[0];
30
+ if (!h) {
31
+ n = !1;
32
+ return;
33
+ }
34
+ const c = await g(l, e, a, h.seq);
35
+ c && (s.apply({ type: "history", conversationId: e, messages: c.messages, hasMore: c.hasMore }), r.render(s)), n = !1;
36
+ }, i = r.getScrollEl();
37
+ if (!i) return;
38
+ let u = !1;
39
+ setTimeout(() => {
40
+ u = !0;
41
+ }, 300);
42
+ const f = () => {
43
+ u && i.scrollTop < 80 && s.hasMoreHistory && !n && p();
44
+ };
45
+ i.addEventListener("scroll", f, { passive: !0 }), r.setScrollCleanup(() => i.removeEventListener("scroll", f));
46
+ }
47
+ function $() {
48
+ const t = "oc_uid";
49
+ try {
50
+ const a = localStorage.getItem(t);
51
+ if (a) return a;
52
+ const e = `g_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
53
+ return localStorage.setItem(t, e), e;
54
+ } catch {
55
+ return `g_${Math.random().toString(36).slice(2)}`;
56
+ }
57
+ }
58
+ export {
59
+ y as h,
60
+ $ as p,
61
+ M as r
62
+ };
63
+ //# sourceMappingURL=uid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uid.js","sources":["../src/history.ts","../src/uid.ts"],"sourcesContent":["// history.ts — shared REST history-fetch logic for both the guest widget\n// (index.ts) and the agent dashboard (operate.ts).\n//\n// Strategy:\n// • On open: fetch the latest 20 messages. Fast, cheap, covers most chats.\n// • hasMore=true → show a sentinel div at the top of the scroll area.\n// • When the sentinel scrolls into view (IntersectionObserver) fetch the\n// next 20 older messages and prepend — no button click required.\n// • This is the same infinite-scroll-upward pattern used by WhatsApp/Telegram.\n\nimport type { ChatStore } from './store.js'\nimport type { Message, ConversationId } from './protocol/index.js'\nimport type { Renderer } from './renderer.js'\n\nconst PAGE = 20 // messages per fetch — fast first load, smooth pagination\n\n/** Derive the HTTP(S) base origin from a ws(s):// URL. */\nexport function httpBaseFromWsUrl(wsUrl: string): string {\n return wsUrl.replace(/^ws/, 'http').replace(/\\/ws\\/?$/, '')\n}\n\n/** Build the best history URL for the given token context.\n * Staff tokens use /messages (full access); guest tokens use /history. */\nfunction historyUrl(httpBase: string, conversationId: string, beforeSeq: number, limit: number): string[] {\n const qs = `beforeSeq=${beforeSeq}&limit=${limit}`\n return [\n `${httpBase}/conversations/${conversationId}/messages?${qs}`,\n `${httpBase}/conversations/${conversationId}/history?${qs}`,\n ]\n}\n\n/** Fetch one page of history. Tries staff endpoint first, falls back to guest. */\nasync function fetchPage(\n httpBase: string,\n conversationId: string,\n token: string,\n beforeSeq: number,\n limit = PAGE,\n): Promise<{ messages: Message[]; hasMore: boolean } | null> {\n for (const url of historyUrl(httpBase, conversationId, beforeSeq, limit)) {\n try {\n const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } })\n if (!res.ok) continue\n const data = await res.json() as { messages?: Message[]; hasMore?: boolean }\n return { messages: data.messages ?? [], hasMore: data.hasMore ?? false }\n } catch { /* try next */ }\n }\n return null\n}\n\n/** Initial history restore on conversation open.\n *\n * Fetches the latest PAGE messages and sets up scroll-triggered loading for\n * older messages via IntersectionObserver on a sentinel at the top of the\n * scroll container. No buttons — scrolling up loads more automatically.\n *\n * Returns a cleanup function — call it when the conversation is closed to\n * disconnect the observer and prevent stale updates. */\nexport async function restoreHistory(\n wsUrl: string,\n token: string,\n conversationId: ConversationId,\n store: ChatStore,\n renderer: Renderer,\n): Promise<void> {\n const httpBase = httpBaseFromWsUrl(wsUrl)\n\n const page = await fetchPage(httpBase, conversationId as string, token, Number.MAX_SAFE_INTEGER)\n if (!page) return\n\n if (page.messages.length) {\n store.apply({ type: 'sync', conversationId, messages: page.messages })\n // Always set hasMore from the response\n store.apply({ type: 'history', conversationId, messages: [], hasMore: page.hasMore })\n renderer.render(store)\n } else {\n // No messages — still record hasMore=false so the sentinel doesn't show\n store.apply({ type: 'history', conversationId, messages: [], hasMore: false })\n }\n\n if (!page.hasMore) return\n\n // ── Scroll-triggered load-more ────────────────────────────────────────────\n // Watch the sentinel div (the \"↑ Load earlier messages\" button rendered by\n // the Renderer at the top of the scroll area). When it becomes visible,\n // fetch the next page of older messages.\n let loading = false\n\n const loadOlder = async () => {\n if (loading || !store.hasMoreHistory) return\n loading = true\n const oldest = store.messages()[0]\n if (!oldest) { loading = false; return }\n const page2 = await fetchPage(httpBase, conversationId as string, token, oldest.seq)\n if (page2) {\n store.apply({ type: 'history', conversationId, messages: page2.messages, hasMore: page2.hasMore })\n renderer.render(store)\n }\n loading = false\n }\n\n // Use IntersectionObserver to detect when the user scrolls to the top.\n // We observe the scroll container itself — when scrollTop < 40px, load more.\n const scrollEl = renderer.getScrollEl()\n if (!scrollEl) return\n\n // Wait 300ms before arming the scroll listener — the initial render scrolls\n // to the bottom, which briefly passes through scrollTop=0 and could trigger\n // a spurious load before the user actually scrolls up.\n let armed = false\n setTimeout(() => { armed = true }, 300)\n\n const onScroll = () => {\n if (!armed) return\n if (scrollEl.scrollTop < 80 && store.hasMoreHistory && !loading) {\n void loadOlder()\n }\n }\n scrollEl.addEventListener('scroll', onScroll, { passive: true })\n renderer.setScrollCleanup(() => scrollEl.removeEventListener('scroll', onScroll))\n}\n","/** Persistent anonymous identity, reused across reloads (per the old widget's\n * oc_uid pattern). */\nexport function persistentUid(): string {\n const key = 'oc_uid'\n try {\n const existing = localStorage.getItem(key)\n if (existing) return existing\n const id = `g_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n localStorage.setItem(key, id)\n return id\n } catch {\n return `g_${Math.random().toString(36).slice(2)}`\n }\n}\n"],"names":["httpBaseFromWsUrl","wsUrl","historyUrl","httpBase","conversationId","beforeSeq","limit","qs","fetchPage","token","url","res","data","restoreHistory","store","renderer","page","loading","loadOlder","oldest","page2","scrollEl","armed","onScroll","persistentUid","key","existing","id"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paramms/chat-widget",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Embeddable real-time chat widget for the Relay platform",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,6 +21,12 @@
21
21
  "dist/react.js",
22
22
  "dist/react.js.map",
23
23
  "dist/react.d.ts",
24
+ "dist/chatlist.js",
25
+ "dist/chatlist.js.map",
26
+ "dist/chatlist.d.ts",
27
+ "dist/uid.js",
28
+ "dist/uid.js.map",
29
+ "dist/uid.d.ts",
24
30
  "dist/store.d.ts",
25
31
  "dist/connection.d.ts",
26
32
  "dist/history.d.ts",
@@ -60,6 +66,10 @@
60
66
  "./react": {
61
67
  "types": "./dist/react.d.ts",
62
68
  "default": "./dist/react.js"
69
+ },
70
+ "./chatlist": {
71
+ "types": "./dist/chatlist.d.ts",
72
+ "default": "./dist/chatlist.js"
63
73
  }
64
74
  },
65
75
  "peerDependencies": {