@paramms/chat-widget 0.1.0 → 1.0.0
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/README.md +138 -12
- package/dist/connection.d.ts +48 -0
- package/dist/crypto.d.ts +69 -0
- package/dist/e2e.d.ts +75 -0
- package/dist/history.d.ts +14 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +2597 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox.d.ts +20 -0
- package/dist/protocol/actions.d.ts +98 -0
- package/dist/protocol/codec.d.ts +12 -0
- package/dist/protocol/entities.d.ts +109 -0
- package/dist/protocol/frames.d.ts +248 -0
- package/dist/protocol/ids.d.ts +22 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/react.d.ts +109 -0
- package/dist/react.js +137 -0
- package/dist/react.js.map +1 -0
- package/dist/renderer.d.ts +159 -0
- package/dist/store.d.ts +48 -0
- package/package.json +24 -1
- package/build-preview.js +0 -136
- package/index.html +0 -37
- package/src/__tests__/chatlist.test.ts +0 -133
- package/src/__tests__/connection.test.ts +0 -163
- package/src/__tests__/crypto.test.ts +0 -28
- package/src/__tests__/history.test.ts +0 -91
- package/src/__tests__/ime.test.ts +0 -93
- package/src/__tests__/render.test.ts +0 -58
- package/src/__tests__/render_new.test.ts +0 -441
- package/src/__tests__/store.test.ts +0 -86
- package/src/__tests__/x3dh.test.ts +0 -204
- package/src/connection.ts +0 -133
- package/src/crypto.ts +0 -252
- package/src/e2e.ts +0 -161
- package/src/history.ts +0 -43
- package/src/index.ts +0 -380
- package/src/outbox.ts +0 -58
- package/src/protocol/actions.ts +0 -114
- package/src/protocol/codec.ts +0 -35
- package/src/protocol/entities.ts +0 -104
- package/src/protocol/frames.ts +0 -86
- package/src/protocol/ids.ts +0 -27
- package/src/protocol/index.ts +0 -5
- package/src/react.tsx +0 -37
- package/src/renderer.ts +0 -906
- package/src/store.ts +0 -207
- package/tsconfig.json +0 -33
- package/vercel.json +0 -22
- package/vite.config.ts +0 -26
- package/vitest.config.ts +0 -2
package/dist/react.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsx as k, Fragment as $ } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as y, useEffect as j } from "react";
|
|
3
|
+
import { mount as E } from "./index.js";
|
|
4
|
+
function D({
|
|
5
|
+
url: l,
|
|
6
|
+
profileId: m,
|
|
7
|
+
userId: t,
|
|
8
|
+
userName: i,
|
|
9
|
+
userEmail: c,
|
|
10
|
+
userAvatar: u,
|
|
11
|
+
contextTitle: p,
|
|
12
|
+
contextSubtitle: o,
|
|
13
|
+
contextStatus: h,
|
|
14
|
+
showChatList: b,
|
|
15
|
+
subjectId: n,
|
|
16
|
+
accent: C,
|
|
17
|
+
launcher: r,
|
|
18
|
+
position: _,
|
|
19
|
+
quickReplies: L,
|
|
20
|
+
height: s = "100%",
|
|
21
|
+
i18n: w,
|
|
22
|
+
translateLang: R
|
|
23
|
+
}) {
|
|
24
|
+
const e = y(null), f = y(null);
|
|
25
|
+
return j(() => {
|
|
26
|
+
if (e.current)
|
|
27
|
+
return f.current = E({
|
|
28
|
+
el: e.current,
|
|
29
|
+
url: l,
|
|
30
|
+
profileId: m,
|
|
31
|
+
...n ? { subjectId: n } : {},
|
|
32
|
+
...t ? { token: t } : {},
|
|
33
|
+
...t || i || c || u ? {
|
|
34
|
+
user: {
|
|
35
|
+
...i ? { name: i } : {},
|
|
36
|
+
...c ? { email: c } : {},
|
|
37
|
+
...u ? { avatar: u } : {}
|
|
38
|
+
}
|
|
39
|
+
} : {},
|
|
40
|
+
...p ? {
|
|
41
|
+
subject: {
|
|
42
|
+
title: p,
|
|
43
|
+
...o ? { subtitle: o } : {},
|
|
44
|
+
...h ? { status: h } : {}
|
|
45
|
+
}
|
|
46
|
+
} : {},
|
|
47
|
+
...b ? { showChatList: !0 } : {},
|
|
48
|
+
...L ? { quickReplies: L } : {},
|
|
49
|
+
...C ? { accent: C } : {},
|
|
50
|
+
...R ? { translateLang: R } : {},
|
|
51
|
+
...w ? { i18n: w } : {},
|
|
52
|
+
...r ? { launcher: r, position: _ ?? "bottom-right" } : {}
|
|
53
|
+
}), () => {
|
|
54
|
+
var v;
|
|
55
|
+
(v = f.current) == null || v.close(), f.current = null;
|
|
56
|
+
};
|
|
57
|
+
}, [l, m, n, t]), r ? /* @__PURE__ */ k($, {}) : /* @__PURE__ */ k(
|
|
58
|
+
"div",
|
|
59
|
+
{
|
|
60
|
+
ref: e,
|
|
61
|
+
style: { width: "100%", height: s, minHeight: s === "100%" ? "480px" : void 0 }
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const F = [
|
|
66
|
+
"Is this still available?",
|
|
67
|
+
"Can I schedule a viewing?",
|
|
68
|
+
"What is your best price?"
|
|
69
|
+
];
|
|
70
|
+
function K({
|
|
71
|
+
url: l,
|
|
72
|
+
profileId: m,
|
|
73
|
+
listingId: t,
|
|
74
|
+
listingTitle: i,
|
|
75
|
+
listingMeta: c,
|
|
76
|
+
listingPrice: u,
|
|
77
|
+
listingStatus: p,
|
|
78
|
+
userId: o,
|
|
79
|
+
userName: h,
|
|
80
|
+
userEmail: b,
|
|
81
|
+
userAvatar: n,
|
|
82
|
+
accent: C,
|
|
83
|
+
launcher: r,
|
|
84
|
+
position: _,
|
|
85
|
+
quickReplies: L,
|
|
86
|
+
height: s = "100%",
|
|
87
|
+
i18n: w,
|
|
88
|
+
translateLang: R
|
|
89
|
+
}) {
|
|
90
|
+
const e = y(null), f = y(null);
|
|
91
|
+
return j(() => {
|
|
92
|
+
if (e.current)
|
|
93
|
+
return f.current = E({
|
|
94
|
+
el: e.current,
|
|
95
|
+
url: l,
|
|
96
|
+
profileId: m,
|
|
97
|
+
...t ? { subjectId: `listing_${t}` } : {},
|
|
98
|
+
...o ? { token: o } : {},
|
|
99
|
+
...o || h || b || n ? {
|
|
100
|
+
user: {
|
|
101
|
+
...h ? { name: h } : {},
|
|
102
|
+
...b ? { email: b } : {},
|
|
103
|
+
...n ? { avatar: n } : {},
|
|
104
|
+
...t ? { meta: { listingId: t } } : {}
|
|
105
|
+
}
|
|
106
|
+
} : {},
|
|
107
|
+
...i ? {
|
|
108
|
+
subject: {
|
|
109
|
+
title: i,
|
|
110
|
+
...c ? { subtitle: c } : {},
|
|
111
|
+
...u ? { tags: [`$${u.toLocaleString()}`] } : {},
|
|
112
|
+
...p ? { status: p } : {}
|
|
113
|
+
}
|
|
114
|
+
} : {},
|
|
115
|
+
showChatList: !0,
|
|
116
|
+
quickReplies: L ?? F,
|
|
117
|
+
...C ? { accent: C } : {},
|
|
118
|
+
...R ? { translateLang: R } : {},
|
|
119
|
+
...w ? { i18n: w } : {},
|
|
120
|
+
...r ? { launcher: r, position: _ ?? "bottom-right" } : {}
|
|
121
|
+
}), () => {
|
|
122
|
+
var v;
|
|
123
|
+
(v = f.current) == null || v.close(), f.current = null;
|
|
124
|
+
};
|
|
125
|
+
}, [l, m, t, o]), r ? /* @__PURE__ */ k($, {}) : /* @__PURE__ */ k(
|
|
126
|
+
"div",
|
|
127
|
+
{
|
|
128
|
+
ref: e,
|
|
129
|
+
style: { width: "100%", height: s, minHeight: s === "100%" ? "480px" : void 0 }
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
D as ChatWidget,
|
|
135
|
+
K as MarketplaceChat
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +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 if (launcher) return <></>\n return (\n <div\n ref={ref}\n style={{ 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 — one thread per listing, guests can switch between all\n * their active threads via a WhatsApp-style list. Only `url` and `profileId`\n * are required. All other props are optional.\n *\n * @example Used car listing\n * ```tsx\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.transmission}`}\n * listingStatus=\"Available\"\n * userId={session?.user.id}\n * userName={session?.user.name}\n * userEmail={session?.user.email}\n * />\n * ```\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 ...(listingId ? { subjectId: `listing_${listingId}` } : {}),\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 ...(listingTitle ? {\n subject: {\n title: listingTitle,\n ...(listingMeta ? { subtitle: listingMeta } : {}),\n ...(listingPrice ? { tags: [`$${listingPrice.toLocaleString()}`] } : {}),\n ...(listingStatus ? { status: listingStatus } : {}),\n },\n } : {}),\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 if (launcher) return <></>\n return (\n <div\n ref={ref}\n style={{ 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","Fragment","DEFAULT_MARKETPLACE_REPLIES","MarketplaceChat","listingId","listingTitle","listingMeta","listingPrice","listingStatus"],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { AnnotationStroke } from './protocol/index.js';
|
|
2
|
+
import type { ChatStore } from './store.js';
|
|
3
|
+
export interface WidgetConfig {
|
|
4
|
+
subject?: {
|
|
5
|
+
title?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
status?: string;
|
|
9
|
+
ownerLabel?: string;
|
|
10
|
+
};
|
|
11
|
+
quickReplies?: string[];
|
|
12
|
+
accent?: string;
|
|
13
|
+
/** Identified user info — shown as the guest avatar/name in the widget header. */
|
|
14
|
+
userInfo?: {
|
|
15
|
+
name?: string;
|
|
16
|
+
avatar?: string;
|
|
17
|
+
};
|
|
18
|
+
/** i18n string overrides */
|
|
19
|
+
i18n?: {
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
send?: string;
|
|
22
|
+
offline?: string;
|
|
23
|
+
poweredBy?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface RendererHandlers {
|
|
27
|
+
onSend(text: string): void;
|
|
28
|
+
onAttach?(file: File): void;
|
|
29
|
+
onInvoke(actionId: string, inputs?: Record<string, unknown>): void;
|
|
30
|
+
onTyping(isTyping: boolean): void;
|
|
31
|
+
onReadUpTo(seq: number): void;
|
|
32
|
+
onReact?(messageId: string, emoji: string, remove: boolean): void;
|
|
33
|
+
onCsat?(score: number): void;
|
|
34
|
+
onLoadMore?(): void;
|
|
35
|
+
onEdit?(messageId: string, newText: string): void;
|
|
36
|
+
onDelete?(messageId: string): void;
|
|
37
|
+
/** Co-browsing: a freehand stroke was completed on the shared whiteboard. */
|
|
38
|
+
onAnnotate?(stroke: Omit<AnnotationStroke, 'by'>): void;
|
|
39
|
+
/** Co-browsing: clear the shared whiteboard for everyone. */
|
|
40
|
+
onAnnotateClear?(): void;
|
|
41
|
+
/** Translate a message's text for display. Return null if unavailable —
|
|
42
|
+
* the renderer shows a brief "unavailable" hint and leaves the original. */
|
|
43
|
+
onTranslate?(text: string): Promise<string | null>;
|
|
44
|
+
/** Multi-subject chat list (e.g. a marketplace with one thread per item):
|
|
45
|
+
* fetch the guest's other conversations. Omit to hide the list button
|
|
46
|
+
* entirely — single-conversation embeds don't need this. */
|
|
47
|
+
onListChats?(): Promise<ChatListEntry[]>;
|
|
48
|
+
/** Switch the active conversation to a different one from the list —
|
|
49
|
+
* effectively a re-open with a different subjectId. */
|
|
50
|
+
onSwitchChat?(entry: ChatListEntry): void;
|
|
51
|
+
}
|
|
52
|
+
export interface ChatListEntry {
|
|
53
|
+
id: string;
|
|
54
|
+
subjectId?: string;
|
|
55
|
+
subjectTitle?: string;
|
|
56
|
+
state: string;
|
|
57
|
+
updatedAt: number;
|
|
58
|
+
/** Server-assigned highest message seq — used to compute unread count. */
|
|
59
|
+
lastSeq?: number;
|
|
60
|
+
/** Snippet of the last message text — shown as preview in the list row. */
|
|
61
|
+
lastMessage?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Renders a ChatStore into a host element in the Image-1 layout: header →
|
|
64
|
+
* subject card → action chips → chat → quick replies → input. Self-injects its
|
|
65
|
+
* stylesheet so it looks right wherever it mounts. Accent comes from the
|
|
66
|
+
* dashboard-authored profile theme (via the manifest), falling back to config. */
|
|
67
|
+
export declare class Renderer {
|
|
68
|
+
private readonly root;
|
|
69
|
+
private readonly me;
|
|
70
|
+
private readonly h;
|
|
71
|
+
private readonly cfg;
|
|
72
|
+
private readonly scroll;
|
|
73
|
+
private readonly chips;
|
|
74
|
+
private readonly typing;
|
|
75
|
+
private readonly quick;
|
|
76
|
+
private readonly formHost;
|
|
77
|
+
private readonly csatPanel;
|
|
78
|
+
private readonly offlinePanel;
|
|
79
|
+
private readonly footer;
|
|
80
|
+
private readonly input;
|
|
81
|
+
private readonly subjectCard;
|
|
82
|
+
private readonly e2eBadge;
|
|
83
|
+
private readonly statusBadge;
|
|
84
|
+
private readonly headerName;
|
|
85
|
+
private readonly connStatus;
|
|
86
|
+
private typingTimer;
|
|
87
|
+
private csatSubmitted;
|
|
88
|
+
private readonly cobrowseBtn;
|
|
89
|
+
private readonly cobrowseCanvas;
|
|
90
|
+
private readonly cobrowseToolbar;
|
|
91
|
+
private readonly cobrowseHint;
|
|
92
|
+
private cobrowseActive;
|
|
93
|
+
private cobrowseColor;
|
|
94
|
+
private lastAnnotationVersion;
|
|
95
|
+
private storeRef;
|
|
96
|
+
private scrollCleanup;
|
|
97
|
+
/** Returns the scroll container so history.ts can attach scroll listeners. */
|
|
98
|
+
getScrollEl(): HTMLElement | null;
|
|
99
|
+
/** Registers a cleanup fn removed on destroy() to prevent listener leaks. */
|
|
100
|
+
setScrollCleanup(fn: () => void): void;
|
|
101
|
+
private readonly translationCache;
|
|
102
|
+
private readonly showingTranslation;
|
|
103
|
+
private listScreen;
|
|
104
|
+
private chatScreen;
|
|
105
|
+
private listBody;
|
|
106
|
+
private listSearch;
|
|
107
|
+
private backBtn;
|
|
108
|
+
private activeChatId;
|
|
109
|
+
private inChatScreen;
|
|
110
|
+
private allEntries;
|
|
111
|
+
/** Last seq the guest has seen per conversationId — used to compute unread badges. */
|
|
112
|
+
private readonly seenSeq;
|
|
113
|
+
private seenSeqKey;
|
|
114
|
+
/** Load seenSeq from localStorage (keyed by userId+profileId). */
|
|
115
|
+
initSeenSeq(key: string): void;
|
|
116
|
+
setSeenSeq(conversationId: string, seq: number): void;
|
|
117
|
+
constructor(root: HTMLElement, me: string, h: RendererHandlers, cfg?: WidgetConfig);
|
|
118
|
+
/** Fetch the list and re-render it. Called on mount and after switching chats. */
|
|
119
|
+
private destroyed;
|
|
120
|
+
/** Call when the widget is unmounted. Disconnects scroll listeners and
|
|
121
|
+
* prevents in-flight fetches from writing to dead DOM. */
|
|
122
|
+
destroy(): void;
|
|
123
|
+
refreshList(): void;
|
|
124
|
+
/** Render list rows, optionally filtered by search query. */
|
|
125
|
+
private renderListRows;
|
|
126
|
+
private isUnread;
|
|
127
|
+
/** Build a single WhatsApp-style conversation row. */
|
|
128
|
+
private buildRow;
|
|
129
|
+
/** Returns true when the chat screen is visible (not the list). */
|
|
130
|
+
isInChatScreen(): boolean;
|
|
131
|
+
/** Navigate to the chat screen (slide list left, slide chat in from right). */
|
|
132
|
+
showChatScreen(): void;
|
|
133
|
+
/** Navigate back to the list screen. */
|
|
134
|
+
showListScreen(): void;
|
|
135
|
+
private flushSend;
|
|
136
|
+
private signalTyping;
|
|
137
|
+
render(store: ChatStore): void;
|
|
138
|
+
setConnStatus(status: 'connecting' | 'open' | 'reconnecting'): void;
|
|
139
|
+
private buildOfflinePanel;
|
|
140
|
+
private buildCsatPanel;
|
|
141
|
+
private subjectBuilt;
|
|
142
|
+
/** Subject card from the server's Subject entity (per chatroom), with the
|
|
143
|
+
* mount config as a fallback. Built once when data first arrives. */
|
|
144
|
+
private updateCobrowseUI;
|
|
145
|
+
private resizeCobrowseCanvas;
|
|
146
|
+
/** Draw a stroke whose points are normalized to 0..1, scaled to the current
|
|
147
|
+
* canvas size — so strokes line up across different viewport sizes. */
|
|
148
|
+
private drawStroke;
|
|
149
|
+
private redrawCobrowse;
|
|
150
|
+
private bindCobrowsePointerEvents;
|
|
151
|
+
private buildSubjectCard;
|
|
152
|
+
private chipEl;
|
|
153
|
+
/** In-widget confirmation modal (replaces window.confirm). */
|
|
154
|
+
private confirm;
|
|
155
|
+
/** Inline form for a form-effect action: typed inputs (date picker, number,
|
|
156
|
+
* text) rendered above the composer — no browser prompts. */
|
|
157
|
+
private openForm;
|
|
158
|
+
private messageEl;
|
|
159
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ServerFrame, Message, ManifestAction, MessageContent, ConversationId, UserId, Subject, AnnotationStroke } from './protocol/index.js';
|
|
2
|
+
export type SendStatus = 'pending' | 'sent' | 'delivered' | 'read';
|
|
3
|
+
export interface RenderMessage extends Message {
|
|
4
|
+
clientMsgId?: string;
|
|
5
|
+
status?: SendStatus;
|
|
6
|
+
}
|
|
7
|
+
/** Pure, DOM-free conversation state. Feed it ServerFrames (and local optimistic
|
|
8
|
+
* sends); read an ordered, de-duplicated view out. Ordering is by `seq`; the
|
|
9
|
+
* same message arriving twice (live + sync on reconnect) is collapsed by id —
|
|
10
|
+
* the structural fix for the old duplicate-bubble bug. */
|
|
11
|
+
export declare class ChatStore {
|
|
12
|
+
private readonly me;
|
|
13
|
+
conversationId?: ConversationId;
|
|
14
|
+
state: string;
|
|
15
|
+
version: number;
|
|
16
|
+
hasMoreHistory: boolean;
|
|
17
|
+
lastReadByOthers: number;
|
|
18
|
+
assignedAgentId: UserId | undefined;
|
|
19
|
+
accent: string | undefined;
|
|
20
|
+
subject: Subject | undefined;
|
|
21
|
+
name: string | undefined;
|
|
22
|
+
e2e: boolean;
|
|
23
|
+
offline: boolean;
|
|
24
|
+
offlineMessage: string;
|
|
25
|
+
whiteLabel: boolean;
|
|
26
|
+
readonly typing: Set<string>;
|
|
27
|
+
readonly online: Set<string>;
|
|
28
|
+
/** Co-browsing: shared whiteboard strokes for this conversation. */
|
|
29
|
+
annotations: AnnotationStroke[];
|
|
30
|
+
/** Bumped on any annotation change so the renderer can cheaply detect updates. */
|
|
31
|
+
annotationVersion: number;
|
|
32
|
+
/** Live sentiment of the guest's latest message (agent-side only). */
|
|
33
|
+
sentiment: 'positive' | 'neutral' | 'frustrated' | undefined;
|
|
34
|
+
sentimentScore: number | undefined;
|
|
35
|
+
private actions;
|
|
36
|
+
private readonly byId;
|
|
37
|
+
private readonly keyByClient;
|
|
38
|
+
private _maxSeq;
|
|
39
|
+
private _sorted;
|
|
40
|
+
constructor(me: UserId);
|
|
41
|
+
messages(): RenderMessage[];
|
|
42
|
+
visibleActions(): ManifestAction[];
|
|
43
|
+
highestSeq(): number;
|
|
44
|
+
addOptimistic(clientMsgId: string, content: MessageContent): RenderMessage;
|
|
45
|
+
apply(frame: ServerFrame): void;
|
|
46
|
+
private upsert;
|
|
47
|
+
private markOwnStatus;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paramms/chat-widget",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Embeddable real-time chat widget for the Relay platform",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/paramms/objectchat"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["chat", "widget", "relay", "customer-support", "live-chat"],
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/index.js",
|
|
13
|
+
"dist/index.js.map",
|
|
14
|
+
"dist/index.d.ts",
|
|
15
|
+
"dist/react.js",
|
|
16
|
+
"dist/react.js.map",
|
|
17
|
+
"dist/react.d.ts",
|
|
18
|
+
"dist/store.d.ts",
|
|
19
|
+
"dist/connection.d.ts",
|
|
20
|
+
"dist/history.d.ts",
|
|
21
|
+
"dist/outbox.d.ts",
|
|
22
|
+
"dist/renderer.d.ts",
|
|
23
|
+
"dist/crypto.d.ts",
|
|
24
|
+
"dist/e2e.d.ts",
|
|
25
|
+
"dist/protocol"
|
|
26
|
+
],
|
|
4
27
|
"type": "module",
|
|
5
28
|
"scripts": {
|
|
6
29
|
"build": "vite build && tsc -p tsconfig.json --emitDeclarationOnly && node build-preview.js",
|
package/build-preview.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// build-preview.js — generates dist/index.html after vite library build.
|
|
2
|
-
//
|
|
3
|
-
// Two modes, chosen by URL at load time (not build time):
|
|
4
|
-
// - relay.paramms.com → marketing/install-guide landing page
|
|
5
|
-
// - relay.paramms.com/?profileId=x → just the widget, in a contained card
|
|
6
|
-
// (this is the URL the dashboard's "Preview" link and shared test links
|
|
7
|
-
// use — should look exactly like the widget would look embedded on a
|
|
8
|
-
// real page, not a full-bleed app)
|
|
9
|
-
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
10
|
-
|
|
11
|
-
mkdirSync('dist', { recursive: true })
|
|
12
|
-
|
|
13
|
-
const html = `<!doctype html>
|
|
14
|
-
<html lang="en">
|
|
15
|
-
<head>
|
|
16
|
-
<meta charset="utf-8" />
|
|
17
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18
|
-
<title>Relay Chat Widget</title>
|
|
19
|
-
<style>
|
|
20
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
-
html, body { height: 100%; }
|
|
22
|
-
body {
|
|
23
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
24
|
-
min-height: 100vh;
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
justify-content: center;
|
|
28
|
-
padding: 24px;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* Marketing mode (no profileId in URL): purple gradient + info column */
|
|
32
|
-
body.marketing {
|
|
33
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
34
|
-
}
|
|
35
|
-
.container {
|
|
36
|
-
max-width: 900px;
|
|
37
|
-
width: 100%;
|
|
38
|
-
display: flex;
|
|
39
|
-
gap: 48px;
|
|
40
|
-
align-items: flex-start;
|
|
41
|
-
}
|
|
42
|
-
.info { flex: 1; color: #fff; }
|
|
43
|
-
.info h1 { font-size: 36px; font-weight: 800; margin-bottom: 12px; }
|
|
44
|
-
.info p { font-size: 16px; opacity: .85; line-height: 1.6; margin-bottom: 24px; }
|
|
45
|
-
.snippet {
|
|
46
|
-
background: rgba(0,0,0,.3);
|
|
47
|
-
border-radius: 12px;
|
|
48
|
-
padding: 20px;
|
|
49
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
50
|
-
font-size: 13px;
|
|
51
|
-
color: #e2e8f0;
|
|
52
|
-
line-height: 1.6;
|
|
53
|
-
white-space: pre;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* Widget-only mode (?profileId=... present): bare contained card on a
|
|
57
|
-
neutral backdrop — what the widget actually looks like embedded on a
|
|
58
|
-
real page, just centered for easy viewing/sharing as a test link. */
|
|
59
|
-
body.widget-only { background: #e9eaee; }
|
|
60
|
-
|
|
61
|
-
/* Both modes use the same card shape for the widget itself */
|
|
62
|
-
.preview {
|
|
63
|
-
width: 390px;
|
|
64
|
-
height: 680px;
|
|
65
|
-
max-width: calc(100vw - 48px);
|
|
66
|
-
max-height: calc(100vh - 48px);
|
|
67
|
-
border-radius: 22px;
|
|
68
|
-
overflow: hidden;
|
|
69
|
-
box-shadow: 0 24px 60px rgba(0,0,0,.25);
|
|
70
|
-
background: #fff;
|
|
71
|
-
flex-shrink: 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
#app { width: 100%; height: 100%; }
|
|
75
|
-
</style>
|
|
76
|
-
</head>
|
|
77
|
-
<body>
|
|
78
|
-
<div id="marketing-info" class="info" style="display:none">
|
|
79
|
-
<h1>Relay Chat Widget</h1>
|
|
80
|
-
<p>Add a real-time chat widget to any website in two lines of code.</p>
|
|
81
|
-
<div class="snippet"><div id="chat"></div>
|
|
82
|
-
<script type="module">
|
|
83
|
-
import { mount } from 'https://relay.paramms.com/index.js'
|
|
84
|
-
mount({
|
|
85
|
-
el: document.getElementById('chat'),
|
|
86
|
-
url: 'wss://api.paramms.com/ws',
|
|
87
|
-
profileId: 'YOUR_PROFILE_ID',
|
|
88
|
-
})
|
|
89
|
-
</script></div>
|
|
90
|
-
</div>
|
|
91
|
-
<div id="layout"></div>
|
|
92
|
-
<script type="module">
|
|
93
|
-
import { mount } from './index.js'
|
|
94
|
-
|
|
95
|
-
const q = new URLSearchParams(location.search)
|
|
96
|
-
const profileId = q.get('profileId')
|
|
97
|
-
const isWidgetOnly = !!profileId
|
|
98
|
-
|
|
99
|
-
document.body.className = isWidgetOnly ? 'widget-only' : 'marketing'
|
|
100
|
-
|
|
101
|
-
const layout = document.getElementById('layout')
|
|
102
|
-
const preview = document.createElement('div')
|
|
103
|
-
preview.className = 'preview'
|
|
104
|
-
const app = document.createElement('div')
|
|
105
|
-
app.id = 'app'
|
|
106
|
-
preview.append(app)
|
|
107
|
-
|
|
108
|
-
if (isWidgetOnly) {
|
|
109
|
-
// Just the contained card, centered on a neutral backdrop.
|
|
110
|
-
layout.append(preview)
|
|
111
|
-
} else {
|
|
112
|
-
// Marketing landing page: info column + card preview, side by side.
|
|
113
|
-
const container = document.createElement('div')
|
|
114
|
-
container.className = 'container'
|
|
115
|
-
const info = document.getElementById('marketing-info')
|
|
116
|
-
info.style.display = ''
|
|
117
|
-
container.append(info, preview)
|
|
118
|
-
layout.append(container)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const wsUrl = q.get('url') || 'wss://api.paramms.com/ws'
|
|
122
|
-
const subjectId = q.get('subjectId') || undefined
|
|
123
|
-
mount({
|
|
124
|
-
el: app,
|
|
125
|
-
url: wsUrl,
|
|
126
|
-
profileId: profileId || 'p_hotel',
|
|
127
|
-
...(subjectId ? { subjectId } : {}),
|
|
128
|
-
accent: q.get('accent') || '#4F63F5',
|
|
129
|
-
showChatList: q.get('chatList') !== 'off',
|
|
130
|
-
})
|
|
131
|
-
</script>
|
|
132
|
-
</body>
|
|
133
|
-
</html>`
|
|
134
|
-
|
|
135
|
-
writeFileSync('dist/index.html', html)
|
|
136
|
-
console.log('✓ dist/index.html written')
|
package/index.html
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>Relay widget</title>
|
|
7
|
-
<style>
|
|
8
|
-
body { margin:0; background:#e9ebef; font-family:system-ui,sans-serif; }
|
|
9
|
-
#app { width:390px; height:720px; margin:24px auto; border-radius:22px; overflow:hidden;
|
|
10
|
-
box-shadow:0 18px 50px rgba(0,0,0,.18); background:#fff; }
|
|
11
|
-
</style>
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
<div id="app"></div>
|
|
15
|
-
<script type="module">
|
|
16
|
-
import { mount } from './src/index.ts'
|
|
17
|
-
const q = new URLSearchParams(location.search)
|
|
18
|
-
const subjectId = q.get('subjectId') || undefined
|
|
19
|
-
const title = q.get('title')
|
|
20
|
-
mount({
|
|
21
|
-
el: document.getElementById('app'),
|
|
22
|
-
url: q.get('url') || import.meta.env.VITE_SERVER_URL || 'ws://localhost:3000/ws',
|
|
23
|
-
profileId: q.get('profileId') || import.meta.env.VITE_PROFILE_ID || 'p_hotel',
|
|
24
|
-
...(subjectId ? { subjectId } : {}),
|
|
25
|
-
accent: q.get('accent') || '#4F63F5',
|
|
26
|
-
...(title ? { subject: {
|
|
27
|
-
ownerLabel: q.get('owner') || undefined,
|
|
28
|
-
title,
|
|
29
|
-
subtitle: q.get('subtitle') || '',
|
|
30
|
-
status: q.get('status') || '',
|
|
31
|
-
tags: (q.get('tags') || '').split(',').filter(Boolean),
|
|
32
|
-
} } : {}),
|
|
33
|
-
quickReplies: (q.get('quick') || '').split(',').filter(Boolean),
|
|
34
|
-
})
|
|
35
|
-
</script>
|
|
36
|
-
</body>
|
|
37
|
-
</html>
|