@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/src/protocol/actions.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { ActionId, ProfileId, TenantId } from './ids.js'
|
|
2
|
-
|
|
3
|
-
// ── Actions: the product primitive ────────────────────────────────────────────
|
|
4
|
-
// An action is data an admin authors in the dashboard; the runtime stays generic
|
|
5
|
-
// and only knows how to execute a small, fixed set of EFFECTS. Adding "make
|
|
6
|
-
// offer" or "schedule meeting" is a config row, not a code deploy.
|
|
7
|
-
|
|
8
|
-
export type ActionAudience = 'guest' | 'agent' | 'both'
|
|
9
|
-
export type ActionSurface = 'toolbar' | 'inline' | 'quick_reply'
|
|
10
|
-
|
|
11
|
-
export interface ActionInputField {
|
|
12
|
-
name: string
|
|
13
|
-
label: string
|
|
14
|
-
type: 'text' | 'number' | 'date' | 'select'
|
|
15
|
-
required?: boolean
|
|
16
|
-
options?: string[] // for type: 'select'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// A terminal effect produces a result and ends the action. Actions are
|
|
20
|
-
// single-shot: structured multi-step lives in the conversation state machine,
|
|
21
|
-
// and conversational multi-step is the `bot` effect — not an action workflow.
|
|
22
|
-
export type TerminalEffect =
|
|
23
|
-
| { type: 'webhook'; url: string } // signed POST to tenant system
|
|
24
|
-
| { type: 'state_transition'; target: 'conversation' | 'subject'; toState: string }
|
|
25
|
-
| { type: 'bot' } // route to the AI resolver
|
|
26
|
-
| { type: 'builtin'; name: string } // e.g. 'handoff'
|
|
27
|
-
|
|
28
|
-
// The ONLY composition allowed is "collect a form, then run one terminal
|
|
29
|
-
// effect" — exactly one level deep. This covers input-gathering (e.g. an offer
|
|
30
|
-
// amount) without becoming a workflow engine.
|
|
31
|
-
export type ActionEffect =
|
|
32
|
-
| TerminalEffect
|
|
33
|
-
| { type: 'form'; fields: ActionInputField[]; then: TerminalEffect }
|
|
34
|
-
|
|
35
|
-
export type ActionResult =
|
|
36
|
-
| { kind: 'system_message'; template?: string } // post a system line into the chat
|
|
37
|
-
| { kind: 'card' } // render the effect's response as a card
|
|
38
|
-
| { kind: 'state_badge' } // reflect a state change
|
|
39
|
-
| { kind: 'none' }
|
|
40
|
-
|
|
41
|
-
export interface ActionDef {
|
|
42
|
-
id: ActionId
|
|
43
|
-
label: string
|
|
44
|
-
icon?: string
|
|
45
|
-
confirm?: boolean
|
|
46
|
-
audience: ActionAudience
|
|
47
|
-
surface: ActionSurface
|
|
48
|
-
availableInStates?: string[] // conversation/subject states; omit = always available
|
|
49
|
-
effect: ActionEffect
|
|
50
|
-
result: ActionResult
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Client-safe projection of an action: enough for the widget to render it and
|
|
54
|
-
// collect inputs, but NONE of the effect internals (webhook URLs, transition
|
|
55
|
-
// targets) — those stay server-side and execute on `invoke`. The client filters
|
|
56
|
-
// by `availableInStates` locally against the current conversation state, so a
|
|
57
|
-
// state change needs no manifest round-trip; the server re-validates on invoke.
|
|
58
|
-
export interface ManifestAction {
|
|
59
|
-
id: ActionId
|
|
60
|
-
label: string
|
|
61
|
-
icon?: string
|
|
62
|
-
confirm?: boolean
|
|
63
|
-
audience: ActionAudience
|
|
64
|
-
surface: ActionSurface
|
|
65
|
-
availableInStates?: string[]
|
|
66
|
-
input?: ActionInputField[] // present when the action collects input (form effect)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Project an internal action to its client-safe manifest form. */
|
|
70
|
-
export function toManifestAction(a: ActionDef): ManifestAction {
|
|
71
|
-
const input = a.effect.type === 'form' ? a.effect.fields : undefined
|
|
72
|
-
return {
|
|
73
|
-
id: a.id, label: a.label, audience: a.audience, surface: a.surface,
|
|
74
|
-
...(a.icon ? { icon: a.icon } : {}),
|
|
75
|
-
...(a.confirm ? { confirm: a.confirm } : {}),
|
|
76
|
-
...(a.availableInStates ? { availableInStates: a.availableInStates } : {}),
|
|
77
|
-
...(input ? { input } : {}),
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── Behavior profile (what "domain" becomes) ──────────────────────────────────
|
|
82
|
-
// A reusable, admin-composed bundle of actions + defaults + state machine. Not a
|
|
83
|
-
// built-in taxonomy — the 7 old templates become starter presets of this shape.
|
|
84
|
-
// `version` lets an in-flight invocation validate against a consistent snapshot.
|
|
85
|
-
|
|
86
|
-
/** Operating hours slot: 0=Sun … 6=Sat, times in "HH:MM" 24h local. */
|
|
87
|
-
export interface OperatingHoursSlot { day: 0|1|2|3|4|5|6; open: string; close: string }
|
|
88
|
-
|
|
89
|
-
export interface BehaviorProfile {
|
|
90
|
-
id: ProfileId
|
|
91
|
-
tenantId: TenantId
|
|
92
|
-
name: string
|
|
93
|
-
actions: ActionDef[]
|
|
94
|
-
defaults: {
|
|
95
|
-
greeting?: string
|
|
96
|
-
theme?: { accent: string }
|
|
97
|
-
e2e?: boolean
|
|
98
|
-
persona?: string
|
|
99
|
-
/** Paid-tier flag: when true, hides the "Powered by Relay" footer in the widget. */
|
|
100
|
-
whiteLabel?: boolean
|
|
101
|
-
/** White-label: serve/embed the widget from this hostname (e.g.
|
|
102
|
-
* "chat.acmeco.com"). Allowed automatically as a CORS origin for the
|
|
103
|
-
* control-plane API so the widget works from the custom domain. */
|
|
104
|
-
customDomain?: string
|
|
105
|
-
}
|
|
106
|
-
states: string[]
|
|
107
|
-
initialState: string
|
|
108
|
-
version: number
|
|
109
|
-
welcomeMessage?: string // first message guests see when opening the widget
|
|
110
|
-
operatingHours?: OperatingHoursSlot[] // empty/absent = always open
|
|
111
|
-
offlineMessage?: string // shown outside operating hours instead of chat
|
|
112
|
-
createdAt: number
|
|
113
|
-
updatedAt: number
|
|
114
|
-
}
|
package/src/protocol/codec.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { encode as mpEncode, decode as mpDecode } from '@msgpack/msgpack'
|
|
2
|
-
import type { ClientFrame, ServerFrame } from './frames.js'
|
|
3
|
-
|
|
4
|
-
export type AnyFrame = ClientFrame | ServerFrame
|
|
5
|
-
|
|
6
|
-
const CLIENT_FRAME_TYPES: ReadonlySet<ClientFrame['type']> = new Set([
|
|
7
|
-
'auth', 'open', 'send', 'sync', 'history', 'read', 'typing', 'react', 'edit', 'delete', 'invoke', 'pubkey', 'ping', 'annotate', 'annotate_clear',
|
|
8
|
-
])
|
|
9
|
-
|
|
10
|
-
/** True if a decoded frame is one a client is allowed to send. The server uses
|
|
11
|
-
* this to reject server-only frame types before dispatch, so a malicious or
|
|
12
|
-
* buggy client can't reach an unexpected handler path. */
|
|
13
|
-
export function isClientFrame(frame: AnyFrame): frame is ClientFrame {
|
|
14
|
-
return CLIENT_FRAME_TYPES.has(frame.type as ClientFrame['type'])
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Encode a frame to a binary msgpack payload for the wire. */
|
|
18
|
-
export function encodeFrame(frame: AnyFrame): Uint8Array {
|
|
19
|
-
return mpEncode(frame)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Decode a binary payload into a frame. Returns null on any malformed input or
|
|
23
|
-
* anything lacking a string `type`, so a bad frame can never crash the handler
|
|
24
|
-
* — the boundary validates `type` before trusting the rest. */
|
|
25
|
-
export function decodeFrame(bytes: Uint8Array): AnyFrame | null {
|
|
26
|
-
let value: unknown
|
|
27
|
-
try {
|
|
28
|
-
value = mpDecode(bytes)
|
|
29
|
-
} catch {
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
if (typeof value !== 'object' || value === null) return null
|
|
33
|
-
if (typeof (value as { type?: unknown }).type !== 'string') return null
|
|
34
|
-
return value as AnyFrame
|
|
35
|
-
}
|
package/src/protocol/entities.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ConversationId, MessageId, ProfileId, SubjectId, TenantId, UserId,
|
|
3
|
-
} from './ids.js'
|
|
4
|
-
import type { ActionId } from './ids.js'
|
|
5
|
-
|
|
6
|
-
// ── Message content (discriminated union) ─────────────────────────────────────
|
|
7
|
-
// The runtime stays generic by never hard-coding business content: a message is
|
|
8
|
-
// one of a small, fixed set of shapes. `card`/`form`/`system` are how action
|
|
9
|
-
// results and structured prompts render — they subsume most "rich messaging"
|
|
10
|
-
// features without a per-feature content zoo.
|
|
11
|
-
|
|
12
|
-
export interface CardField { label: string; value: string }
|
|
13
|
-
|
|
14
|
-
/** A reference to an action a card/quick-reply can invoke. */
|
|
15
|
-
export interface InlineActionRef { actionId: ActionId; label: string }
|
|
16
|
-
|
|
17
|
-
export type MessageContent =
|
|
18
|
-
| { kind: 'text'; text: string; enc?: boolean; iv?: string }
|
|
19
|
-
| { kind: 'attachment'; url: string; mime: string; name?: string; size?: number }
|
|
20
|
-
| { kind: 'card'; title?: string; body?: string; fields?: CardField[]; actions?: InlineActionRef[] }
|
|
21
|
-
| { kind: 'form'; prompt: string; actionId: ActionId }
|
|
22
|
-
| { kind: 'system'; event: string; data?: Record<string, string | number | boolean> }
|
|
23
|
-
| { kind: 'appointment'; title: string; startIso: string; endIso: string; location?: string; description?: string; googleUrl: string; icalUrl: string; confirmed?: boolean }
|
|
24
|
-
|
|
25
|
-
export type SenderRole = 'guest' | 'agent' | 'system' | 'bot'
|
|
26
|
-
|
|
27
|
-
// ── Message ───────────────────────────────────────────────────────────────────
|
|
28
|
-
// Ordering is by `seq` (server-assigned, monotonic per conversation), never by
|
|
29
|
-
// `ts`. `ts` is wall-clock for display only. This kills the reorder/duplicate/
|
|
30
|
-
// lost-on-reconnect class of bugs that millisecond-timestamp ordering caused.
|
|
31
|
-
|
|
32
|
-
export interface Message {
|
|
33
|
-
id: MessageId
|
|
34
|
-
conversationId: ConversationId
|
|
35
|
-
seq: number
|
|
36
|
-
senderId: UserId
|
|
37
|
-
senderRole: SenderRole
|
|
38
|
-
content: MessageContent
|
|
39
|
-
ts: number
|
|
40
|
-
replyToId?: MessageId
|
|
41
|
-
editedAt?: number
|
|
42
|
-
deletedAt?: number
|
|
43
|
-
reactions?: Record<string, UserId[]>
|
|
44
|
-
internal?: boolean // true = internal note, only visible to agents
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Conversation (the room; messages partition by conversationId) ─────────────
|
|
48
|
-
// The room is the conversation, NOT the subject. Two guests discussing the same
|
|
49
|
-
// subject get two conversations. `subjectId` is a nullable reference, never part
|
|
50
|
-
// of the room identity — so "one thread per (guest, subject)" is enforced as
|
|
51
|
-
// app logic at open-time, and many-threads-per-subject stays possible for free.
|
|
52
|
-
|
|
53
|
-
export interface Conversation {
|
|
54
|
-
id: ConversationId
|
|
55
|
-
tenantId: TenantId
|
|
56
|
-
profileId: ProfileId // behavior profile → which actions this room has
|
|
57
|
-
subjectId?: SubjectId // optional: the thing it's about
|
|
58
|
-
guestId: UserId // the end-user
|
|
59
|
-
participants: UserId[] // guest + any assigned agents (membership = authz)
|
|
60
|
-
assignedAgentId?: UserId // routing/ownership
|
|
61
|
-
aiActive?: boolean // staff assigned the AI to answer this room
|
|
62
|
-
state: string // conversation state-machine state
|
|
63
|
-
firstResponseAt?: number // first agent reply ts (SLA)
|
|
64
|
-
csat?: number // satisfaction score 1–5 (set on resolution)
|
|
65
|
-
lastSeq: number // highest seq assigned in this conversation
|
|
66
|
-
tags?: string[] // macro/manual tags (e.g. "refund", "vip")
|
|
67
|
-
/** Live sentiment of the guest's most recent message (best-effort, async). */
|
|
68
|
-
sentiment?: 'positive' | 'neutral' | 'frustrated'
|
|
69
|
-
/** -1 (very frustrated) .. +1 (very positive); paired with `sentiment`. */
|
|
70
|
-
sentimentScore?: number
|
|
71
|
-
/** Set once an SLA-breach escalation macro has fired, so it only fires once. */
|
|
72
|
-
slaEscalatedAt?: number
|
|
73
|
-
createdAt: number
|
|
74
|
-
updatedAt: number
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Co-browsing / shared annotation ────────────────────────────────────────---
|
|
78
|
-
// A lightweight shared whiteboard layered over a conversation: agent and guest
|
|
79
|
-
// can draw freehand strokes that both sides see live. Strokes are relayed
|
|
80
|
-
// (not stored as messages) and kept per-conversation so late joiners can catch
|
|
81
|
-
// up via the `opened` frame's `annotations` field.
|
|
82
|
-
|
|
83
|
-
export interface AnnotationPoint { x: number; y: number }
|
|
84
|
-
export interface AnnotationStroke {
|
|
85
|
-
id: string
|
|
86
|
-
points: AnnotationPoint[]
|
|
87
|
-
color: string
|
|
88
|
-
width: number
|
|
89
|
-
by: UserId
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── Subject (the referenced entity — Intercom "custom object") ────────────────
|
|
93
|
-
// Carries shared state (available → reserved → sold) and fields (price, vin…)
|
|
94
|
-
// that actions read/write. Many conversations reference one subject. Never a room.
|
|
95
|
-
|
|
96
|
-
export interface Subject {
|
|
97
|
-
id: SubjectId
|
|
98
|
-
tenantId: TenantId
|
|
99
|
-
title: string
|
|
100
|
-
state: string
|
|
101
|
-
fields: Record<string, string | number | boolean>
|
|
102
|
-
createdAt: number
|
|
103
|
-
updatedAt: number
|
|
104
|
-
}
|
package/src/protocol/frames.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ConnectionId, ConversationId, MessageId, ProfileId, SubjectId, UserId,
|
|
3
|
-
} from './ids.js'
|
|
4
|
-
import type { Conversation, Message, MessageContent, Subject, AnnotationStroke } from './entities.js'
|
|
5
|
-
import type { ManifestAction } from './actions.js'
|
|
6
|
-
|
|
7
|
-
// ── Wire protocol ─────────────────────────────────────────────────────────────
|
|
8
|
-
// One shared contract, imported by server + widget + dashboard. A change here is
|
|
9
|
-
// a compile error in every consumer — which is the whole reason this lives in a
|
|
10
|
-
// shared package instead of being hand-copied three times.
|
|
11
|
-
|
|
12
|
-
export type ErrorCode =
|
|
13
|
-
| 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'BAD_REQUEST'
|
|
14
|
-
| 'RATE_LIMITED' | 'PAYLOAD_TOO_LARGE' | 'CONFLICT' | 'INTERNAL'
|
|
15
|
-
|
|
16
|
-
export type ClientFrame =
|
|
17
|
-
| { type: 'auth'; token: string }
|
|
18
|
-
// Open an existing conversation, or find-or-create one. Find-or-create keys on
|
|
19
|
-
// (guest, subject) when subjectId is given; otherwise a fresh conversation.
|
|
20
|
-
| { type: 'open'; conversationId?: ConversationId; subjectId?: SubjectId; profileId?: ProfileId }
|
|
21
|
-
| { type: 'send'; conversationId: ConversationId; clientMsgId: string; content: MessageContent; replyToId?: MessageId }
|
|
22
|
-
| { type: 'sync'; conversationId: ConversationId; sinceSeq: number } // catch-up after cursor
|
|
23
|
-
| { type: 'history'; conversationId: ConversationId; beforeSeq: number; limit?: number } // load older
|
|
24
|
-
| { type: 'read'; conversationId: ConversationId; seq: number } // read up to seq
|
|
25
|
-
| { type: 'typing'; conversationId: ConversationId; isTyping: boolean }
|
|
26
|
-
| { type: 'react'; conversationId: ConversationId; messageId: MessageId; emoji: string; remove?: boolean }
|
|
27
|
-
| { type: 'edit'; conversationId: ConversationId; messageId: MessageId; content: MessageContent }
|
|
28
|
-
| { type: 'delete'; conversationId: ConversationId; messageId: MessageId }
|
|
29
|
-
| { type: 'invoke'; conversationId: ConversationId; actionId: string; clientInvokeId: string; inputs?: Record<string, unknown> }
|
|
30
|
-
| { type: 'assign'; conversationId: ConversationId; agentId: UserId | null } // null = unassign
|
|
31
|
-
| { type: 'tag'; conversationId: ConversationId; tag: string; remove?: boolean }
|
|
32
|
-
| { type: 'note'; conversationId: ConversationId; clientMsgId: string; text: string } // internal note
|
|
33
|
-
| { type: 'agent_status'; status: 'online' | 'away' | 'offline' } // agent sets their availability
|
|
34
|
-
// Co-browsing: a freehand stroke (or "clear") on the shared annotation canvas
|
|
35
|
-
// for a subject-anchored conversation. Relayed live to the other participant.
|
|
36
|
-
| { type: 'annotate'; conversationId: ConversationId; stroke: Omit<AnnotationStroke, 'by'> }
|
|
37
|
-
| { type: 'annotate_clear'; conversationId: ConversationId }
|
|
38
|
-
| { type: 'pubkey'; conversationId: ConversationId; key: string }
|
|
39
|
-
// X3DH async E2E: a client uploads a batch of one-time prekeys so peers can
|
|
40
|
-
// encrypt to them while they are offline. The server stores them opaquely and
|
|
41
|
-
// vends one on demand — it never derives or uses the keys.
|
|
42
|
-
| { type: 'uploadPrekeys'; identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekeys: string[] }
|
|
43
|
-
| { type: 'fetchPrekey'; targetUserId: UserId }
|
|
44
|
-
| { type: 'ping' }
|
|
45
|
-
|
|
46
|
-
export type ServerFrame =
|
|
47
|
-
| { type: 'authed'; userId: UserId; connectionId: ConnectionId }
|
|
48
|
-
| { type: 'opened'; conversation: Conversation; subject?: Subject; annotations?: AnnotationStroke[] }
|
|
49
|
-
| { type: 'manifest'; conversationId: ConversationId; actions: ManifestAction[]; version: number; name?: string; theme?: { accent: string }; e2e?: boolean; offline?: boolean; offlineMessage?: string; whiteLabel?: boolean }
|
|
50
|
-
| { type: 'message'; message: Message }
|
|
51
|
-
| { type: 'ack'; clientMsgId: string; messageId: MessageId; seq: number; ts: number }
|
|
52
|
-
| { type: 'delivered'; conversationId: ConversationId; seq: number; to: UserId }
|
|
53
|
-
| { type: 'read'; conversationId: ConversationId; seq: number; by: UserId }
|
|
54
|
-
| { type: 'sync'; conversationId: ConversationId; messages: Message[] }
|
|
55
|
-
| { type: 'history'; conversationId: ConversationId; messages: Message[]; hasMore: boolean }
|
|
56
|
-
| { type: 'typing'; conversationId: ConversationId; userId: UserId; isTyping: boolean }
|
|
57
|
-
| { type: 'reaction'; conversationId: ConversationId; messageId: MessageId; emoji: string; by: UserId; removed: boolean }
|
|
58
|
-
| { type: 'edited'; conversationId: ConversationId; messageId: MessageId; content: MessageContent; editedAt: number }
|
|
59
|
-
| { type: 'deleted'; conversationId: ConversationId; messageId: MessageId; ts: number }
|
|
60
|
-
| { type: 'state'; conversationId: ConversationId; state: string }
|
|
61
|
-
| { type: 'assigned'; conversationId: ConversationId; agentId: UserId | null }
|
|
62
|
-
| { type: 'tagged'; conversationId: ConversationId; tag: string; removed: boolean }
|
|
63
|
-
| { type: 'visitor_count'; count: number } // broadcast to agents: guests currently connected
|
|
64
|
-
| { type: 'agent_status_changed'; agentId: UserId; status: 'online' | 'away' | 'offline' }
|
|
65
|
-
// Live sentiment of a guest's most recent message — relayed to agents only so
|
|
66
|
-
// the inbox can flag frustrated conversations as they happen.
|
|
67
|
-
| { type: 'sentiment'; conversationId: ConversationId; label: 'positive' | 'neutral' | 'frustrated'; score: number }
|
|
68
|
-
// Co-browsing: relay of an annotation stroke / clear to everyone in the room.
|
|
69
|
-
| { type: 'annotation'; conversationId: ConversationId; stroke: AnnotationStroke }
|
|
70
|
-
| { type: 'annotation_clear'; conversationId: ConversationId; by: UserId }
|
|
71
|
-
| { type: 'subjectState'; subjectId: SubjectId; state: string }
|
|
72
|
-
| { type: 'presence'; conversationId: ConversationId; userId: UserId; status: 'online' | 'offline'; lastSeen?: number }
|
|
73
|
-
| { type: 'invoked'; clientInvokeId: string; ok: boolean; error?: string }
|
|
74
|
-
| { type: 'error'; code: ErrorCode; message: string }
|
|
75
|
-
| { type: 'peerkey'; conversationId: ConversationId; userId: UserId; key: string }
|
|
76
|
-
// X3DH bundle vended to a requesting client so they can encrypt to an offline peer.
|
|
77
|
-
// Contains null when the target user has no registered prekeys.
|
|
78
|
-
| { type: 'prekeyBundle'; targetUserId: UserId; bundle: { identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekey?: string } | null }
|
|
79
|
-
| { type: 'pong' }
|
|
80
|
-
|
|
81
|
-
/** Limits referenced by both ends so validation stays consistent. */
|
|
82
|
-
export const LIMITS = {
|
|
83
|
-
MAX_TEXT_LEN: 8_000,
|
|
84
|
-
MAX_HISTORY_LIMIT: 100,
|
|
85
|
-
DEFAULT_HISTORY: 50,
|
|
86
|
-
} as const
|
package/src/protocol/ids.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// Branded identifier types. A plain string can't be passed where a TenantId is
|
|
2
|
-
// expected (and vice-versa), so id-confusion bugs — the kind that caused the
|
|
3
|
-
// `t-1` vs `dev-tenant` mismatch in the previous system — become compile errors.
|
|
4
|
-
|
|
5
|
-
export type Brand<T, B extends string> = T & { readonly __brand: B }
|
|
6
|
-
|
|
7
|
-
export type TenantId = Brand<string, 'TenantId'>
|
|
8
|
-
export type UserId = Brand<string, 'UserId'>
|
|
9
|
-
export type ConversationId = Brand<string, 'ConversationId'>
|
|
10
|
-
export type SubjectId = Brand<string, 'SubjectId'>
|
|
11
|
-
export type MessageId = Brand<string, 'MessageId'>
|
|
12
|
-
export type ActionId = Brand<string, 'ActionId'>
|
|
13
|
-
export type ProfileId = Brand<string, 'ProfileId'>
|
|
14
|
-
export type ConnectionId = Brand<string, 'ConnectionId'>
|
|
15
|
-
|
|
16
|
-
export const asTenantId = (s: string): TenantId => s as TenantId
|
|
17
|
-
export const asUserId = (s: string): UserId => s as UserId
|
|
18
|
-
export const asConversationId = (s: string): ConversationId => s as ConversationId
|
|
19
|
-
export const asSubjectId = (s: string): SubjectId => s as SubjectId
|
|
20
|
-
export const asMessageId = (s: string): MessageId => s as MessageId
|
|
21
|
-
export const asActionId = (s: string): ActionId => s as ActionId
|
|
22
|
-
export const asProfileId = (s: string): ProfileId => s as ProfileId
|
|
23
|
-
export const asConnectionId = (s: string): ConnectionId => s as ConnectionId
|
|
24
|
-
|
|
25
|
-
/** Anonymous guests live under this tenant; they bypass cross-tenant scoping but
|
|
26
|
-
* are still gated by conversation membership. */
|
|
27
|
-
export const ANONYMOUS_TENANT = asTenantId('anonymous')
|
package/src/protocol/index.ts
DELETED
package/src/react.tsx
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// react.tsx — React wrapper around mount(). Optional: only needed if you're
|
|
2
|
-
// using React/Next.js. Plain HTML/vanilla JS sites should use mount() directly.
|
|
3
|
-
//
|
|
4
|
-
// Usage:
|
|
5
|
-
// import { ChatWidget } from '@paramms/chat-widget/react'
|
|
6
|
-
// <ChatWidget url="wss://api.paramms.com/ws" profileId="p_xxx" />
|
|
7
|
-
//
|
|
8
|
-
// Next.js (App Router): this component uses browser APIs (WebSocket, DOM) and
|
|
9
|
-
// MUST be rendered client-side. Add the 'use client' directive to whichever
|
|
10
|
-
// file renders it, e.g.:
|
|
11
|
-
// 'use client'
|
|
12
|
-
// import { ChatWidget } from '@paramms/chat-widget/react'
|
|
13
|
-
import { useEffect, useRef } from 'react'
|
|
14
|
-
import { mount, type MountOptions, type WidgetHandle } from './index.js'
|
|
15
|
-
|
|
16
|
-
export type ChatWidgetProps = Omit<MountOptions, 'el'>
|
|
17
|
-
|
|
18
|
-
/** Drop-in React component for the chat widget. Mounts on the client only —
|
|
19
|
-
* safe to use in Next.js as long as the host file/component is marked
|
|
20
|
-
* 'use client' (the widget needs WebSocket + DOM, which don't exist during
|
|
21
|
-
* server-side rendering). */
|
|
22
|
-
export function ChatWidget(props: ChatWidgetProps): JSX.Element {
|
|
23
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
24
|
-
const handleRef = useRef<WidgetHandle | null>(null)
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (!ref.current) return
|
|
28
|
-
handleRef.current = mount({ ...props, el: ref.current })
|
|
29
|
-
return () => { handleRef.current?.close(); handleRef.current = null }
|
|
30
|
-
// Re-mount if the connection target or profile changes; most other props
|
|
31
|
-
// (theme, quick replies) are read once at mount time by design, matching
|
|
32
|
-
// how the vanilla mount() API works.
|
|
33
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
-
}, [props.url, props.profileId, props.subjectId, props.token])
|
|
35
|
-
|
|
36
|
-
return <div ref={ref} style={{ width: '100%', height: '100%' }} />
|
|
37
|
-
}
|