@paramms/chat-widget 1.0.30 → 1.0.31
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/annotations.d.ts +11 -0
- package/dist/core.d.ts +71 -0
- package/dist/core.js +146 -0
- package/dist/core.js.map +1 -0
- package/dist/e2e.js +1608 -0
- package/dist/e2e.js.map +1 -0
- package/dist/hooks.d.ts +43 -0
- package/dist/hooks.js +71 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.html +176 -0
- package/package.json +6 -24
package/dist/index.html
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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 Chat Widget</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
html, body { height: 100%; }
|
|
10
|
+
body {
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
padding: 24px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Marketing mode (no profileId in URL): purple gradient + info column */
|
|
20
|
+
body.marketing {
|
|
21
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
22
|
+
}
|
|
23
|
+
.container {
|
|
24
|
+
max-width: 900px;
|
|
25
|
+
width: 100%;
|
|
26
|
+
display: flex;
|
|
27
|
+
gap: 48px;
|
|
28
|
+
align-items: flex-start;
|
|
29
|
+
}
|
|
30
|
+
.info { flex: 1; color: #fff; }
|
|
31
|
+
.info h1 { font-size: 36px; font-weight: 800; margin-bottom: 12px; }
|
|
32
|
+
.info p { font-size: 16px; opacity: .85; line-height: 1.6; margin-bottom: 24px; }
|
|
33
|
+
.snippet {
|
|
34
|
+
background: rgba(0,0,0,.3);
|
|
35
|
+
border-radius: 12px;
|
|
36
|
+
padding: 20px;
|
|
37
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
color: #e2e8f0;
|
|
40
|
+
line-height: 1.6;
|
|
41
|
+
white-space: pre;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Widget-only mode (?profileId=... present): bare contained card on a
|
|
45
|
+
neutral backdrop — what the widget actually looks like embedded on a
|
|
46
|
+
real page, just centered for easy viewing/sharing as a test link. */
|
|
47
|
+
body.widget-only { background: #e9eaee; }
|
|
48
|
+
|
|
49
|
+
/* Both modes use the same card shape for the widget itself */
|
|
50
|
+
.preview {
|
|
51
|
+
width: 390px;
|
|
52
|
+
height: 680px;
|
|
53
|
+
max-width: calc(100vw - 48px);
|
|
54
|
+
max-height: calc(100vh - 48px);
|
|
55
|
+
border-radius: 22px;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
box-shadow: 0 24px 60px rgba(0,0,0,.25);
|
|
58
|
+
background: #fff;
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#app { width: 100%; height: 100%; }
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div id="marketing-info" class="info" style="display:none">
|
|
67
|
+
<h1>Relay Chat Widget</h1>
|
|
68
|
+
<p>Add a real-time chat widget to any website in two lines of code.</p>
|
|
69
|
+
<div class="snippet"><div id="chat"></div>
|
|
70
|
+
<script type="module">
|
|
71
|
+
import { mount } from 'https://relay.paramms.com/index.js'
|
|
72
|
+
mount({
|
|
73
|
+
el: document.getElementById('chat'),
|
|
74
|
+
url: 'wss://api.paramms.com/ws',
|
|
75
|
+
profileId: 'YOUR_PROFILE_ID',
|
|
76
|
+
})
|
|
77
|
+
</script></div>
|
|
78
|
+
</div>
|
|
79
|
+
<div id="layout"></div>
|
|
80
|
+
<script type="module">
|
|
81
|
+
import { mount } from './index.js'
|
|
82
|
+
|
|
83
|
+
const q = new URLSearchParams(location.search)
|
|
84
|
+
const profileId = q.get('profileId')
|
|
85
|
+
const isWidgetOnly = !!profileId
|
|
86
|
+
|
|
87
|
+
document.body.className = isWidgetOnly ? 'widget-only' : 'marketing'
|
|
88
|
+
|
|
89
|
+
const layout = document.getElementById('layout')
|
|
90
|
+
const preview = document.createElement('div')
|
|
91
|
+
preview.className = 'preview'
|
|
92
|
+
const app = document.createElement('div')
|
|
93
|
+
app.id = 'app'
|
|
94
|
+
preview.append(app)
|
|
95
|
+
|
|
96
|
+
if (isWidgetOnly) {
|
|
97
|
+
// Just the contained card, centered on a neutral backdrop.
|
|
98
|
+
layout.append(preview)
|
|
99
|
+
} else {
|
|
100
|
+
// Marketing landing page: info column + card preview, side by side.
|
|
101
|
+
const container = document.createElement('div')
|
|
102
|
+
container.className = 'container'
|
|
103
|
+
const info = document.getElementById('marketing-info')
|
|
104
|
+
info.style.display = ''
|
|
105
|
+
container.append(info, preview)
|
|
106
|
+
layout.append(container)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const wsUrl = q.get('url') || 'wss://api.paramms.com/ws'
|
|
110
|
+
const subjectId = q.get('subjectId') || undefined
|
|
111
|
+
|
|
112
|
+
// ── Persistent guest identity ────────────────────────────────────────────
|
|
113
|
+
// Resolved here and persisted via BOTH a first-party cookie and localStorage.
|
|
114
|
+
// On this top-level page the cookie survives refreshes even when localStorage
|
|
115
|
+
// is cleared/blocked, so the guest keeps the same id across reloads and the
|
|
116
|
+
// server resumes their existing conversation instead of starting a new chat.
|
|
117
|
+
function readCookie(name) {
|
|
118
|
+
const m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
|
|
119
|
+
return m ? decodeURIComponent(m[1]) : null
|
|
120
|
+
}
|
|
121
|
+
function persistentGuestId() {
|
|
122
|
+
let id = q.get('uid') // explicit pin wins
|
|
123
|
+
if (!id) { try { id = localStorage.getItem('oc_uid') } catch (e) {} }
|
|
124
|
+
if (!id) id = readCookie('oc_uid')
|
|
125
|
+
if (!id) id = 'g_' + Math.random().toString(36).slice(2) + Date.now().toString(36)
|
|
126
|
+
try { localStorage.setItem('oc_uid', id) } catch (e) {}
|
|
127
|
+
const secure = location.protocol === 'https:' ? '; Secure' : ''
|
|
128
|
+
document.cookie = 'oc_uid=' + encodeURIComponent(id) +
|
|
129
|
+
'; Max-Age=' + (60 * 60 * 24 * 365) + '; Path=/; SameSite=Lax' + secure
|
|
130
|
+
return id
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Prefer SERVER-OWNED identity: the relay mints a signed guest token and
|
|
134
|
+
// sets an httpOnly cookie scoped to the parent domain, so the same guest is
|
|
135
|
+
// recognised across refreshes regardless of client storage. Falls back to
|
|
136
|
+
// the client-persisted id if the relay can't be reached — and NEVER hangs:
|
|
137
|
+
// fetch() has no built-in timeout, so without this a slow/unreachable relay
|
|
138
|
+
// would leave the promise pending forever and the widget would never mount
|
|
139
|
+
// (a blank page). We race it against a short timeout.
|
|
140
|
+
let apiBase = wsUrl.replace('wss://', 'https://').replace('ws://', 'http://')
|
|
141
|
+
if (apiBase.endsWith('/ws')) apiBase = apiBase.slice(0, -3)
|
|
142
|
+
async function resolveToken() {
|
|
143
|
+
const pinned = q.get('token') || q.get('uid') // host-supplied identity wins
|
|
144
|
+
if (pinned) return pinned
|
|
145
|
+
try {
|
|
146
|
+
const ctrl = new AbortController()
|
|
147
|
+
const timer = setTimeout(() => ctrl.abort(), 3000)
|
|
148
|
+
const r = await fetch(apiBase + '/auth/guest', { method: 'POST', credentials: 'include', signal: ctrl.signal })
|
|
149
|
+
clearTimeout(timer)
|
|
150
|
+
if (r.ok) { const d = await r.json(); if (d && d.token) return d.token }
|
|
151
|
+
} catch (e) { /* unreachable/slow/aborted — fall back below */ }
|
|
152
|
+
return persistentGuestId()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Mount only ever depends on a resolved token, and any failure surfaces as a
|
|
156
|
+
// visible message rather than a blank page.
|
|
157
|
+
resolveToken()
|
|
158
|
+
.catch(() => persistentGuestId())
|
|
159
|
+
.then((token) => {
|
|
160
|
+
try {
|
|
161
|
+
mount({
|
|
162
|
+
el: app,
|
|
163
|
+
token,
|
|
164
|
+
url: wsUrl,
|
|
165
|
+
profileId: profileId || 'p_hotel',
|
|
166
|
+
...(subjectId ? { subjectId } : {}),
|
|
167
|
+
accent: q.get('accent') || '#4F63F5',
|
|
168
|
+
})
|
|
169
|
+
} catch (err) {
|
|
170
|
+
app.innerHTML = '<div style="padding:24px;font:14px system-ui;color:#b91c1c">' +
|
|
171
|
+
'Chat failed to load. ' + (err && err.message ? String(err.message) : '') + '</div>'
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
</script>
|
|
175
|
+
</body>
|
|
176
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paramms/chat-widget",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "Embeddable real-time chat widget for the Relay platform",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,29 +15,7 @@
|
|
|
15
15
|
"live-chat"
|
|
16
16
|
],
|
|
17
17
|
"files": [
|
|
18
|
-
"dist
|
|
19
|
-
"dist/index.js.map",
|
|
20
|
-
"dist/index.d.ts",
|
|
21
|
-
"dist/react.js",
|
|
22
|
-
"dist/react.js.map",
|
|
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",
|
|
30
|
-
"dist/embed.js",
|
|
31
|
-
"dist/embed.js.map",
|
|
32
|
-
"dist/embed.d.ts",
|
|
33
|
-
"dist/store.d.ts",
|
|
34
|
-
"dist/connection.d.ts",
|
|
35
|
-
"dist/history.d.ts",
|
|
36
|
-
"dist/outbox.d.ts",
|
|
37
|
-
"dist/renderer.d.ts",
|
|
38
|
-
"dist/crypto.d.ts",
|
|
39
|
-
"dist/e2e.d.ts",
|
|
40
|
-
"dist/protocol"
|
|
18
|
+
"dist"
|
|
41
19
|
],
|
|
42
20
|
"type": "module",
|
|
43
21
|
"scripts": {
|
|
@@ -81,6 +59,10 @@
|
|
|
81
59
|
"./hooks": {
|
|
82
60
|
"types": "./dist/hooks.d.ts",
|
|
83
61
|
"default": "./dist/hooks.js"
|
|
62
|
+
},
|
|
63
|
+
"./e2e": {
|
|
64
|
+
"types": "./dist/e2e.d.ts",
|
|
65
|
+
"default": "./dist/e2e.js"
|
|
84
66
|
}
|
|
85
67
|
},
|
|
86
68
|
"peerDependencies": {
|