@veroai/chat 0.1.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 +579 -0
- package/dist/chunk-JI6KXOLF.js +920 -0
- package/dist/chunk-JI6KXOLF.js.map +1 -0
- package/dist/client-C63XKtNw.d.ts +502 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +187 -0
- package/dist/react/index.js +371 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
// src/api/index.ts
|
|
4
|
+
var ChatApi = class {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
7
|
+
this.getToken = config.getToken;
|
|
8
|
+
}
|
|
9
|
+
async getHeaders() {
|
|
10
|
+
const token = await this.getToken();
|
|
11
|
+
return {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
...token && { Authorization: `Bearer ${token}` }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async handleResponse(response) {
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
const errorText = await response.text();
|
|
19
|
+
let errorMessage;
|
|
20
|
+
try {
|
|
21
|
+
const errorJson = JSON.parse(errorText);
|
|
22
|
+
errorMessage = errorJson.error?.message || errorJson.message || errorText;
|
|
23
|
+
} catch {
|
|
24
|
+
errorMessage = errorText;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(errorMessage);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Conversations
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* List all conversations for the current user
|
|
35
|
+
*/
|
|
36
|
+
async listConversations() {
|
|
37
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations`, {
|
|
38
|
+
headers: await this.getHeaders()
|
|
39
|
+
});
|
|
40
|
+
const data = await this.handleResponse(response);
|
|
41
|
+
return data.conversations.map(transformConversation);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get a specific conversation
|
|
45
|
+
*/
|
|
46
|
+
async getConversation(conversationId) {
|
|
47
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations/${conversationId}`, {
|
|
48
|
+
headers: await this.getHeaders()
|
|
49
|
+
});
|
|
50
|
+
const data = await this.handleResponse(response);
|
|
51
|
+
return transformConversation(data.conversation);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a new conversation
|
|
55
|
+
*/
|
|
56
|
+
async createConversation(params) {
|
|
57
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: await this.getHeaders(),
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
type: params.type || "direct",
|
|
62
|
+
name: params.name,
|
|
63
|
+
participant_ids: params.participantIds,
|
|
64
|
+
agent_config_id: params.agentConfigId,
|
|
65
|
+
metadata: params.metadata
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
const data = await this.handleResponse(response);
|
|
69
|
+
return transformConversation(data.conversation);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mark conversation as read
|
|
73
|
+
*/
|
|
74
|
+
async markConversationRead(conversationId) {
|
|
75
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations/${conversationId}/read`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: await this.getHeaders()
|
|
78
|
+
});
|
|
79
|
+
await this.handleResponse(response);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Leave a conversation
|
|
83
|
+
*/
|
|
84
|
+
async leaveConversation(conversationId) {
|
|
85
|
+
const response = await fetch(
|
|
86
|
+
`${this.apiUrl}/v1/chat/conversations/${conversationId}/participants/me`,
|
|
87
|
+
{
|
|
88
|
+
method: "DELETE",
|
|
89
|
+
headers: await this.getHeaders()
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
await this.handleResponse(response);
|
|
93
|
+
}
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Messages
|
|
96
|
+
// ============================================================================
|
|
97
|
+
/**
|
|
98
|
+
* Get messages for a conversation
|
|
99
|
+
*/
|
|
100
|
+
async getMessages(conversationId, params) {
|
|
101
|
+
const searchParams = new URLSearchParams();
|
|
102
|
+
if (params?.limit) searchParams.set("limit", String(params.limit));
|
|
103
|
+
if (params?.offset) searchParams.set("offset", String(params.offset));
|
|
104
|
+
if (params?.before) searchParams.set("before", params.before);
|
|
105
|
+
const url = `${this.apiUrl}/v1/chat/conversations/${conversationId}/messages?${searchParams}`;
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
headers: await this.getHeaders()
|
|
108
|
+
});
|
|
109
|
+
const data = await this.handleResponse(response);
|
|
110
|
+
return {
|
|
111
|
+
messages: data.messages.map(transformMessage),
|
|
112
|
+
total: data.total,
|
|
113
|
+
hasMore: data.has_more,
|
|
114
|
+
limit: data.limit,
|
|
115
|
+
offset: data.offset
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Send a message to a conversation
|
|
120
|
+
*/
|
|
121
|
+
async sendMessage(conversationId, params) {
|
|
122
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations/${conversationId}/messages`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: await this.getHeaders(),
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
content: params.content,
|
|
127
|
+
message_type: params.messageType || "text",
|
|
128
|
+
metadata: params.metadata
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
const data = await this.handleResponse(response);
|
|
132
|
+
return transformMessage(data.message);
|
|
133
|
+
}
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Users
|
|
136
|
+
// ============================================================================
|
|
137
|
+
/**
|
|
138
|
+
* List all users (contacts)
|
|
139
|
+
*/
|
|
140
|
+
async listUsers(options) {
|
|
141
|
+
const searchParams = new URLSearchParams();
|
|
142
|
+
if (options?.includeVirtual) searchParams.set("include_virtual", "true");
|
|
143
|
+
const url = `${this.apiUrl}/v1/chat/users?${searchParams}`;
|
|
144
|
+
const response = await fetch(url, {
|
|
145
|
+
headers: await this.getHeaders()
|
|
146
|
+
});
|
|
147
|
+
const data = await this.handleResponse(response);
|
|
148
|
+
return data.users.map(transformUser);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get online users
|
|
152
|
+
*/
|
|
153
|
+
async getOnlineUsers() {
|
|
154
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/users/online`, {
|
|
155
|
+
headers: await this.getHeaders()
|
|
156
|
+
});
|
|
157
|
+
const data = await this.handleResponse(response);
|
|
158
|
+
return data.users.map(transformUser);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get current user profile
|
|
162
|
+
*/
|
|
163
|
+
async getCurrentUser() {
|
|
164
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/users/me`, {
|
|
165
|
+
headers: await this.getHeaders()
|
|
166
|
+
});
|
|
167
|
+
const data = await this.handleResponse(response);
|
|
168
|
+
return transformUser(data.user);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get a specific user
|
|
172
|
+
*/
|
|
173
|
+
async getUser(userId) {
|
|
174
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/users/${userId}`, {
|
|
175
|
+
headers: await this.getHeaders()
|
|
176
|
+
});
|
|
177
|
+
const data = await this.handleResponse(response);
|
|
178
|
+
return transformUser(data.user);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Update current user's presence status
|
|
182
|
+
*/
|
|
183
|
+
async updateStatus(status, statusMessage) {
|
|
184
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/users/me/status`, {
|
|
185
|
+
method: "PUT",
|
|
186
|
+
headers: await this.getHeaders(),
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
status,
|
|
189
|
+
status_message: statusMessage
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
await this.handleResponse(response);
|
|
193
|
+
}
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Agents
|
|
196
|
+
// ============================================================================
|
|
197
|
+
/**
|
|
198
|
+
* Add agent to conversation
|
|
199
|
+
*/
|
|
200
|
+
async addAgentToConversation(conversationId, agentConfigId, addAsParticipant = true) {
|
|
201
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations/${conversationId}/agent`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: await this.getHeaders(),
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
agent_config_id: agentConfigId,
|
|
206
|
+
add_as_participant: addAsParticipant
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
await this.handleResponse(response);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Remove agent from conversation
|
|
213
|
+
*/
|
|
214
|
+
async removeAgentFromConversation(conversationId) {
|
|
215
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations/${conversationId}/agent`, {
|
|
216
|
+
method: "DELETE",
|
|
217
|
+
headers: await this.getHeaders()
|
|
218
|
+
});
|
|
219
|
+
await this.handleResponse(response);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Toggle agent enabled/disabled
|
|
223
|
+
*/
|
|
224
|
+
async toggleAgent(conversationId, enabled) {
|
|
225
|
+
const response = await fetch(`${this.apiUrl}/v1/chat/conversations/${conversationId}/agent`, {
|
|
226
|
+
method: "PATCH",
|
|
227
|
+
headers: await this.getHeaders(),
|
|
228
|
+
body: JSON.stringify({ enabled })
|
|
229
|
+
});
|
|
230
|
+
await this.handleResponse(response);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* List available agents
|
|
234
|
+
*/
|
|
235
|
+
async listAgents() {
|
|
236
|
+
const response = await fetch(`${this.apiUrl}/v1/agent-configurations`, {
|
|
237
|
+
headers: await this.getHeaders()
|
|
238
|
+
});
|
|
239
|
+
const data = await this.handleResponse(response);
|
|
240
|
+
return data;
|
|
241
|
+
}
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// Voice Rooms (LiveKit)
|
|
244
|
+
// ============================================================================
|
|
245
|
+
/**
|
|
246
|
+
* Create a new voice/video room
|
|
247
|
+
*/
|
|
248
|
+
async createRoom(params) {
|
|
249
|
+
const response = await fetch(`${this.apiUrl}/v1/voice/rooms`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: await this.getHeaders(),
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
name: params.name,
|
|
254
|
+
empty_timeout: params.emptyTimeout,
|
|
255
|
+
max_participants: params.maxParticipants
|
|
256
|
+
})
|
|
257
|
+
});
|
|
258
|
+
const data = await this.handleResponse(response);
|
|
259
|
+
return transformRoomInfo(data.room);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Join an existing room and get access token
|
|
263
|
+
*/
|
|
264
|
+
async joinRoom(params) {
|
|
265
|
+
const response = await fetch(
|
|
266
|
+
`${this.apiUrl}/v1/voice/rooms/${encodeURIComponent(params.roomName)}/join`,
|
|
267
|
+
{
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: await this.getHeaders(),
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
participant_name: params.participantName,
|
|
272
|
+
can_publish: params.canPublish ?? true,
|
|
273
|
+
can_subscribe: params.canSubscribe ?? true
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
const data = await this.handleResponse(response);
|
|
278
|
+
return {
|
|
279
|
+
name: data.room_name,
|
|
280
|
+
wsUrl: data.ws_url,
|
|
281
|
+
token: data.token
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get room token for an existing room
|
|
286
|
+
* Convenience method for getting a token without creating
|
|
287
|
+
*/
|
|
288
|
+
async getRoomToken(roomName, participantName) {
|
|
289
|
+
return this.joinRoom({ roomName, participantName });
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function transformUser(raw) {
|
|
293
|
+
return {
|
|
294
|
+
id: raw.id,
|
|
295
|
+
email: raw.email,
|
|
296
|
+
firstName: raw.first_name,
|
|
297
|
+
lastName: raw.last_name,
|
|
298
|
+
isVirtual: raw.is_virtual,
|
|
299
|
+
agentConfigId: raw.agent_config_id,
|
|
300
|
+
status: raw.status,
|
|
301
|
+
statusMessage: raw.status_message,
|
|
302
|
+
lastSeen: raw.last_seen,
|
|
303
|
+
createdAt: raw.created_at
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function transformParticipant(raw) {
|
|
307
|
+
return {
|
|
308
|
+
userId: raw.user_id,
|
|
309
|
+
role: raw.role,
|
|
310
|
+
isActive: raw.is_active,
|
|
311
|
+
joinedAt: raw.joined_at,
|
|
312
|
+
lastSeen: raw.last_seen,
|
|
313
|
+
user: raw.user ? transformUser(raw.user) : void 0
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function transformConversation(raw) {
|
|
317
|
+
return {
|
|
318
|
+
id: raw.id,
|
|
319
|
+
name: raw.name,
|
|
320
|
+
type: raw.type,
|
|
321
|
+
isActive: raw.is_active,
|
|
322
|
+
lastMessageAt: raw.last_message_at,
|
|
323
|
+
agentEnabled: raw.agent_enabled,
|
|
324
|
+
agentConfigId: raw.agent_config_id,
|
|
325
|
+
participants: raw.participants?.map(transformParticipant),
|
|
326
|
+
unreadCount: raw.unread_count,
|
|
327
|
+
metadata: raw.metadata,
|
|
328
|
+
createdAt: raw.created_at,
|
|
329
|
+
updatedAt: raw.updated_at
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function transformMessage(raw) {
|
|
333
|
+
return {
|
|
334
|
+
id: raw.id,
|
|
335
|
+
conversationId: raw.conversation_id,
|
|
336
|
+
content: raw.content,
|
|
337
|
+
messageType: raw.message_type,
|
|
338
|
+
senderId: raw.sender_id,
|
|
339
|
+
sender: raw.sender ? transformUser(raw.sender) : void 0,
|
|
340
|
+
readBy: raw.read_by?.map((r) => ({
|
|
341
|
+
userId: r.user_id,
|
|
342
|
+
readAt: r.read_at
|
|
343
|
+
})),
|
|
344
|
+
metadata: raw.metadata,
|
|
345
|
+
createdAt: raw.created_at,
|
|
346
|
+
editedAt: raw.edited_at
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function transformRoomInfo(raw) {
|
|
350
|
+
return {
|
|
351
|
+
name: raw.name,
|
|
352
|
+
wsUrl: raw.ws_url,
|
|
353
|
+
token: raw.token
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
var WebSocketManager = class extends EventEmitter {
|
|
357
|
+
constructor(config) {
|
|
358
|
+
super();
|
|
359
|
+
this.ws = null;
|
|
360
|
+
this.state = "disconnected";
|
|
361
|
+
this.reconnectAttempts = 0;
|
|
362
|
+
this.reconnectTimer = null;
|
|
363
|
+
this.heartbeatTimer = null;
|
|
364
|
+
this.pendingMessages = [];
|
|
365
|
+
this.config = {
|
|
366
|
+
url: config.url,
|
|
367
|
+
getToken: config.getToken,
|
|
368
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
369
|
+
reconnectInterval: config.reconnectInterval ?? 3e3,
|
|
370
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
371
|
+
heartbeatInterval: config.heartbeatInterval ?? 3e4
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get current connection state
|
|
376
|
+
*/
|
|
377
|
+
getState() {
|
|
378
|
+
return this.state;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Check if connected
|
|
382
|
+
*/
|
|
383
|
+
isConnected() {
|
|
384
|
+
return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Connect to the WebSocket server
|
|
388
|
+
*/
|
|
389
|
+
async connect() {
|
|
390
|
+
if (this.state === "connecting" || this.state === "connected") {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this.state = "connecting";
|
|
394
|
+
const token = await this.config.getToken();
|
|
395
|
+
if (!token) {
|
|
396
|
+
this.state = "disconnected";
|
|
397
|
+
throw new Error("No authentication token available");
|
|
398
|
+
}
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
try {
|
|
401
|
+
const url = new URL(this.config.url);
|
|
402
|
+
url.searchParams.set("token", token);
|
|
403
|
+
this.ws = new WebSocket(url.toString());
|
|
404
|
+
this.ws.onopen = () => {
|
|
405
|
+
this.state = "connected";
|
|
406
|
+
this.reconnectAttempts = 0;
|
|
407
|
+
this.startHeartbeat();
|
|
408
|
+
this.flushPendingMessages();
|
|
409
|
+
this.emit("connected");
|
|
410
|
+
resolve();
|
|
411
|
+
};
|
|
412
|
+
this.ws.onclose = (event) => {
|
|
413
|
+
this.handleClose(event.reason);
|
|
414
|
+
};
|
|
415
|
+
this.ws.onerror = (event) => {
|
|
416
|
+
const error = new Error("WebSocket error");
|
|
417
|
+
this.emit("error", error);
|
|
418
|
+
if (this.state === "connecting") {
|
|
419
|
+
reject(error);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
this.ws.onmessage = (event) => {
|
|
423
|
+
this.handleMessage(event.data);
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this.state = "disconnected";
|
|
427
|
+
reject(error);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Disconnect from the WebSocket server
|
|
433
|
+
*/
|
|
434
|
+
disconnect() {
|
|
435
|
+
this.config.autoReconnect = false;
|
|
436
|
+
this.clearTimers();
|
|
437
|
+
if (this.ws) {
|
|
438
|
+
this.ws.close(1e3, "Client disconnect");
|
|
439
|
+
this.ws = null;
|
|
440
|
+
}
|
|
441
|
+
this.state = "disconnected";
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Send a message through the WebSocket
|
|
445
|
+
*/
|
|
446
|
+
send(type, payload) {
|
|
447
|
+
const message = JSON.stringify({ type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
448
|
+
if (this.isConnected()) {
|
|
449
|
+
this.ws.send(message);
|
|
450
|
+
} else {
|
|
451
|
+
this.pendingMessages.push(message);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Send typing indicator
|
|
456
|
+
*/
|
|
457
|
+
sendTypingStart(conversationId) {
|
|
458
|
+
this.send("typing:start", { conversationId });
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Stop typing indicator
|
|
462
|
+
*/
|
|
463
|
+
sendTypingStop(conversationId) {
|
|
464
|
+
this.send("typing:stop", { conversationId });
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Subscribe to a conversation for real-time updates
|
|
468
|
+
*/
|
|
469
|
+
subscribeToConversation(conversationId) {
|
|
470
|
+
this.send("subscribe", { conversationId });
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Unsubscribe from a conversation
|
|
474
|
+
*/
|
|
475
|
+
unsubscribeFromConversation(conversationId) {
|
|
476
|
+
this.send("unsubscribe", { conversationId });
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Update presence status
|
|
480
|
+
*/
|
|
481
|
+
updatePresence(status, statusMessage) {
|
|
482
|
+
this.send("presence:update", { status, statusMessage });
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Send call notification (ring, accept, reject, end)
|
|
486
|
+
* Note: Actual WebRTC signaling is handled by LiveKit
|
|
487
|
+
*/
|
|
488
|
+
sendCallNotification(conversationId, action, callType, roomName) {
|
|
489
|
+
this.send("call", { conversationId, action, callType, roomName });
|
|
490
|
+
}
|
|
491
|
+
handleClose(reason) {
|
|
492
|
+
this.stopHeartbeat();
|
|
493
|
+
const wasConnected = this.state === "connected";
|
|
494
|
+
this.state = "disconnected";
|
|
495
|
+
this.ws = null;
|
|
496
|
+
if (wasConnected) {
|
|
497
|
+
this.emit("disconnected", reason);
|
|
498
|
+
}
|
|
499
|
+
if (this.config.autoReconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
|
|
500
|
+
this.scheduleReconnect();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
scheduleReconnect() {
|
|
504
|
+
if (this.reconnectTimer) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
this.state = "reconnecting";
|
|
508
|
+
this.reconnectAttempts++;
|
|
509
|
+
const delay = Math.min(
|
|
510
|
+
this.config.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
|
|
511
|
+
3e4
|
|
512
|
+
// Max 30 seconds
|
|
513
|
+
);
|
|
514
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
515
|
+
this.reconnectTimer = null;
|
|
516
|
+
try {
|
|
517
|
+
await this.connect();
|
|
518
|
+
} catch (error) {
|
|
519
|
+
}
|
|
520
|
+
}, delay);
|
|
521
|
+
}
|
|
522
|
+
handleMessage(data) {
|
|
523
|
+
try {
|
|
524
|
+
const message = JSON.parse(data);
|
|
525
|
+
switch (message.type) {
|
|
526
|
+
case "message:new":
|
|
527
|
+
this.emit("message:new", message.payload);
|
|
528
|
+
break;
|
|
529
|
+
case "message:updated":
|
|
530
|
+
this.emit("message:updated", message.payload);
|
|
531
|
+
break;
|
|
532
|
+
case "message:deleted": {
|
|
533
|
+
const { messageId, conversationId } = message.payload;
|
|
534
|
+
this.emit("message:deleted", messageId, conversationId);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case "conversation:created":
|
|
538
|
+
this.emit("conversation:created", message.payload);
|
|
539
|
+
break;
|
|
540
|
+
case "conversation:updated":
|
|
541
|
+
this.emit("conversation:updated", message.payload);
|
|
542
|
+
break;
|
|
543
|
+
case "participant:joined": {
|
|
544
|
+
const { conversationId, participant } = message.payload;
|
|
545
|
+
this.emit("participant:joined", conversationId, participant);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case "participant:left": {
|
|
549
|
+
const payload = message.payload;
|
|
550
|
+
this.emit("participant:left", payload.conversationId, payload.userId);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case "presence:updated":
|
|
554
|
+
this.emit("presence:updated", message.payload);
|
|
555
|
+
break;
|
|
556
|
+
case "typing:start":
|
|
557
|
+
this.emit("typing:start", message.payload);
|
|
558
|
+
break;
|
|
559
|
+
case "typing:stop":
|
|
560
|
+
this.emit("typing:stop", message.payload);
|
|
561
|
+
break;
|
|
562
|
+
case "read:receipt":
|
|
563
|
+
this.emit("read:receipt", message.payload);
|
|
564
|
+
break;
|
|
565
|
+
case "call:ring":
|
|
566
|
+
this.emit("call:ring", message.payload);
|
|
567
|
+
break;
|
|
568
|
+
case "call:accept":
|
|
569
|
+
this.emit("call:accept", message.payload);
|
|
570
|
+
break;
|
|
571
|
+
case "call:reject":
|
|
572
|
+
this.emit("call:reject", message.payload);
|
|
573
|
+
break;
|
|
574
|
+
case "call:end":
|
|
575
|
+
this.emit("call:end", message.payload);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error("[ChatWS] Failed to parse message:", error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
flushPendingMessages() {
|
|
583
|
+
while (this.pendingMessages.length > 0 && this.isConnected()) {
|
|
584
|
+
const message = this.pendingMessages.shift();
|
|
585
|
+
if (message) {
|
|
586
|
+
this.ws.send(message);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
startHeartbeat() {
|
|
591
|
+
this.stopHeartbeat();
|
|
592
|
+
this.heartbeatTimer = setInterval(() => {
|
|
593
|
+
if (this.isConnected()) {
|
|
594
|
+
this.send("ping", {});
|
|
595
|
+
}
|
|
596
|
+
}, this.config.heartbeatInterval);
|
|
597
|
+
}
|
|
598
|
+
stopHeartbeat() {
|
|
599
|
+
if (this.heartbeatTimer) {
|
|
600
|
+
clearInterval(this.heartbeatTimer);
|
|
601
|
+
this.heartbeatTimer = null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
clearTimers() {
|
|
605
|
+
this.stopHeartbeat();
|
|
606
|
+
if (this.reconnectTimer) {
|
|
607
|
+
clearTimeout(this.reconnectTimer);
|
|
608
|
+
this.reconnectTimer = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
var ChatClient = class extends EventEmitter {
|
|
613
|
+
constructor(config) {
|
|
614
|
+
super();
|
|
615
|
+
this.ws = null;
|
|
616
|
+
this.config = config;
|
|
617
|
+
this.tokenGetter = config.getToken || (() => config.token || null);
|
|
618
|
+
this.api = new ChatApi({
|
|
619
|
+
apiUrl: config.apiUrl,
|
|
620
|
+
getToken: this.tokenGetter
|
|
621
|
+
});
|
|
622
|
+
if (config.wsUrl) {
|
|
623
|
+
this.ws = new WebSocketManager({
|
|
624
|
+
url: config.wsUrl,
|
|
625
|
+
getToken: this.tokenGetter,
|
|
626
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
627
|
+
reconnectInterval: config.reconnectInterval,
|
|
628
|
+
maxReconnectAttempts: config.maxReconnectAttempts
|
|
629
|
+
});
|
|
630
|
+
this.forwardWebSocketEvents();
|
|
631
|
+
}
|
|
632
|
+
if (config.autoConnect && config.wsUrl) {
|
|
633
|
+
this.connect().catch((error) => {
|
|
634
|
+
console.error("[ChatClient] Auto-connect failed:", error);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// ============================================================================
|
|
639
|
+
// Connection Management
|
|
640
|
+
// ============================================================================
|
|
641
|
+
/**
|
|
642
|
+
* Connect to WebSocket for real-time updates
|
|
643
|
+
*/
|
|
644
|
+
async connect() {
|
|
645
|
+
if (!this.ws) {
|
|
646
|
+
throw new Error("WebSocket URL not configured");
|
|
647
|
+
}
|
|
648
|
+
await this.ws.connect();
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Disconnect from WebSocket
|
|
652
|
+
*/
|
|
653
|
+
disconnect() {
|
|
654
|
+
this.ws?.disconnect();
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Check if WebSocket is connected
|
|
658
|
+
*/
|
|
659
|
+
isConnected() {
|
|
660
|
+
return this.ws?.isConnected() ?? false;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Update authentication token
|
|
664
|
+
*/
|
|
665
|
+
setToken(token) {
|
|
666
|
+
this.config.token = token;
|
|
667
|
+
}
|
|
668
|
+
// ============================================================================
|
|
669
|
+
// Conversations
|
|
670
|
+
// ============================================================================
|
|
671
|
+
/**
|
|
672
|
+
* List all conversations for the current user
|
|
673
|
+
*/
|
|
674
|
+
async listConversations() {
|
|
675
|
+
return this.api.listConversations();
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Get a specific conversation
|
|
679
|
+
*/
|
|
680
|
+
async getConversation(conversationId) {
|
|
681
|
+
return this.api.getConversation(conversationId);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Create a new conversation
|
|
685
|
+
*/
|
|
686
|
+
async createConversation(params) {
|
|
687
|
+
const conversation = await this.api.createConversation(params);
|
|
688
|
+
if (this.ws?.isConnected()) {
|
|
689
|
+
this.ws.subscribeToConversation(conversation.id);
|
|
690
|
+
}
|
|
691
|
+
return conversation;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Mark conversation as read
|
|
695
|
+
*/
|
|
696
|
+
async markConversationRead(conversationId) {
|
|
697
|
+
return this.api.markConversationRead(conversationId);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Leave a conversation
|
|
701
|
+
*/
|
|
702
|
+
async leaveConversation(conversationId) {
|
|
703
|
+
if (this.ws?.isConnected()) {
|
|
704
|
+
this.ws.unsubscribeFromConversation(conversationId);
|
|
705
|
+
}
|
|
706
|
+
return this.api.leaveConversation(conversationId);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Subscribe to real-time updates for a conversation
|
|
710
|
+
*/
|
|
711
|
+
subscribeToConversation(conversationId) {
|
|
712
|
+
this.ws?.subscribeToConversation(conversationId);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Unsubscribe from real-time updates for a conversation
|
|
716
|
+
*/
|
|
717
|
+
unsubscribeFromConversation(conversationId) {
|
|
718
|
+
this.ws?.unsubscribeFromConversation(conversationId);
|
|
719
|
+
}
|
|
720
|
+
// ============================================================================
|
|
721
|
+
// Messages
|
|
722
|
+
// ============================================================================
|
|
723
|
+
/**
|
|
724
|
+
* Get messages for a conversation
|
|
725
|
+
*/
|
|
726
|
+
async getMessages(conversationId, params) {
|
|
727
|
+
return this.api.getMessages(conversationId, params);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Send a message to a conversation
|
|
731
|
+
*/
|
|
732
|
+
async sendMessage(conversationId, params) {
|
|
733
|
+
return this.api.sendMessage(conversationId, params);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Send a text message (convenience method)
|
|
737
|
+
*/
|
|
738
|
+
async send(conversationId, content) {
|
|
739
|
+
return this.sendMessage(conversationId, { content });
|
|
740
|
+
}
|
|
741
|
+
// ============================================================================
|
|
742
|
+
// Typing Indicators
|
|
743
|
+
// ============================================================================
|
|
744
|
+
/**
|
|
745
|
+
* Send typing start indicator
|
|
746
|
+
*/
|
|
747
|
+
sendTypingStart(conversationId) {
|
|
748
|
+
this.ws?.sendTypingStart(conversationId);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Send typing stop indicator
|
|
752
|
+
*/
|
|
753
|
+
sendTypingStop(conversationId) {
|
|
754
|
+
this.ws?.sendTypingStop(conversationId);
|
|
755
|
+
}
|
|
756
|
+
// ============================================================================
|
|
757
|
+
// Users & Presence
|
|
758
|
+
// ============================================================================
|
|
759
|
+
/**
|
|
760
|
+
* List all users (contacts)
|
|
761
|
+
*/
|
|
762
|
+
async listUsers(options) {
|
|
763
|
+
return this.api.listUsers(options);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Get online users
|
|
767
|
+
*/
|
|
768
|
+
async getOnlineUsers() {
|
|
769
|
+
return this.api.getOnlineUsers();
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Get current user profile
|
|
773
|
+
*/
|
|
774
|
+
async getCurrentUser() {
|
|
775
|
+
return this.api.getCurrentUser();
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Get a specific user
|
|
779
|
+
*/
|
|
780
|
+
async getUser(userId) {
|
|
781
|
+
return this.api.getUser(userId);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Update current user's presence status
|
|
785
|
+
*/
|
|
786
|
+
async updateStatus(status, statusMessage) {
|
|
787
|
+
await this.api.updateStatus(status, statusMessage);
|
|
788
|
+
this.ws?.updatePresence(status, statusMessage);
|
|
789
|
+
}
|
|
790
|
+
// ============================================================================
|
|
791
|
+
// Agents
|
|
792
|
+
// ============================================================================
|
|
793
|
+
/**
|
|
794
|
+
* Add agent to conversation
|
|
795
|
+
*/
|
|
796
|
+
async addAgentToConversation(conversationId, agentConfigId, addAsParticipant = true) {
|
|
797
|
+
return this.api.addAgentToConversation(conversationId, agentConfigId, addAsParticipant);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Remove agent from conversation
|
|
801
|
+
*/
|
|
802
|
+
async removeAgentFromConversation(conversationId) {
|
|
803
|
+
return this.api.removeAgentFromConversation(conversationId);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Toggle agent enabled/disabled
|
|
807
|
+
*/
|
|
808
|
+
async toggleAgent(conversationId, enabled) {
|
|
809
|
+
return this.api.toggleAgent(conversationId, enabled);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* List available agents
|
|
813
|
+
*/
|
|
814
|
+
async listAgents() {
|
|
815
|
+
return this.api.listAgents();
|
|
816
|
+
}
|
|
817
|
+
// ============================================================================
|
|
818
|
+
// Voice/Video Calls (LiveKit)
|
|
819
|
+
// ============================================================================
|
|
820
|
+
/**
|
|
821
|
+
* Create a new voice/video room
|
|
822
|
+
*
|
|
823
|
+
* @example
|
|
824
|
+
* ```typescript
|
|
825
|
+
* const room = await chat.createRoom({ name: `call-${conversationId}` });
|
|
826
|
+
* // Use room.wsUrl and room.token with LiveKit client
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
async createRoom(params) {
|
|
830
|
+
return this.api.createRoom(params);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Join an existing room and get access token
|
|
834
|
+
*
|
|
835
|
+
* @example
|
|
836
|
+
* ```typescript
|
|
837
|
+
* const room = await chat.joinRoom({
|
|
838
|
+
* roomName: `call-${conversationId}`,
|
|
839
|
+
* participantName: 'John Doe',
|
|
840
|
+
* });
|
|
841
|
+
* // Connect to room using LiveKit client SDK with room.wsUrl and room.token
|
|
842
|
+
* ```
|
|
843
|
+
*/
|
|
844
|
+
async joinRoom(params) {
|
|
845
|
+
return this.api.joinRoom(params);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Initiate a call in a conversation
|
|
849
|
+
* This notifies other participants that you're calling
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* ```typescript
|
|
853
|
+
* // Start a video call
|
|
854
|
+
* const room = await chat.startCall(conversationId, 'video');
|
|
855
|
+
* // room contains { name, wsUrl, token } for LiveKit
|
|
856
|
+
* ```
|
|
857
|
+
*/
|
|
858
|
+
async startCall(conversationId, callType = "audio") {
|
|
859
|
+
const roomName = `call-${conversationId}-${Date.now()}`;
|
|
860
|
+
const room = await this.api.createRoom({ name: roomName });
|
|
861
|
+
this.ws?.sendCallNotification(conversationId, "ring", callType, roomName);
|
|
862
|
+
return room;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Accept an incoming call
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* ```typescript
|
|
869
|
+
* chat.on('call:ring', async ({ conversationId, roomName, callType }) => {
|
|
870
|
+
* const room = await chat.acceptCall(conversationId, roomName);
|
|
871
|
+
* // Connect to room using LiveKit client SDK
|
|
872
|
+
* });
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
875
|
+
async acceptCall(conversationId, roomName, participantName) {
|
|
876
|
+
const room = await this.api.joinRoom({ roomName, participantName });
|
|
877
|
+
this.ws?.sendCallNotification(conversationId, "accept", void 0, roomName);
|
|
878
|
+
return room;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Reject an incoming call
|
|
882
|
+
*/
|
|
883
|
+
rejectCall(conversationId) {
|
|
884
|
+
this.ws?.sendCallNotification(conversationId, "reject");
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* End an ongoing call
|
|
888
|
+
*/
|
|
889
|
+
endCall(conversationId) {
|
|
890
|
+
this.ws?.sendCallNotification(conversationId, "end");
|
|
891
|
+
}
|
|
892
|
+
// ============================================================================
|
|
893
|
+
// Internal
|
|
894
|
+
// ============================================================================
|
|
895
|
+
forwardWebSocketEvents() {
|
|
896
|
+
if (!this.ws) return;
|
|
897
|
+
this.ws.on("connected", () => this.emit("connected"));
|
|
898
|
+
this.ws.on("disconnected", (reason) => this.emit("disconnected", reason));
|
|
899
|
+
this.ws.on("error", (error) => this.emit("error", error));
|
|
900
|
+
this.ws.on("message:new", (event) => this.emit("message:new", event));
|
|
901
|
+
this.ws.on("message:updated", (msg) => this.emit("message:updated", msg));
|
|
902
|
+
this.ws.on("message:deleted", (msgId, convId) => this.emit("message:deleted", msgId, convId));
|
|
903
|
+
this.ws.on("conversation:created", (conv) => this.emit("conversation:created", conv));
|
|
904
|
+
this.ws.on("conversation:updated", (conv) => this.emit("conversation:updated", conv));
|
|
905
|
+
this.ws.on("participant:joined", (convId, p) => this.emit("participant:joined", convId, p));
|
|
906
|
+
this.ws.on("participant:left", (convId, userId) => this.emit("participant:left", convId, userId));
|
|
907
|
+
this.ws.on("presence:updated", (event) => this.emit("presence:updated", event));
|
|
908
|
+
this.ws.on("typing:start", (event) => this.emit("typing:start", event));
|
|
909
|
+
this.ws.on("typing:stop", (event) => this.emit("typing:stop", event));
|
|
910
|
+
this.ws.on("read:receipt", (event) => this.emit("read:receipt", event));
|
|
911
|
+
this.ws.on("call:ring", (event) => this.emit("call:ring", event));
|
|
912
|
+
this.ws.on("call:accept", (event) => this.emit("call:accept", event));
|
|
913
|
+
this.ws.on("call:reject", (event) => this.emit("call:reject", event));
|
|
914
|
+
this.ws.on("call:end", (event) => this.emit("call:end", event));
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
export { ChatApi, ChatClient, WebSocketManager };
|
|
919
|
+
//# sourceMappingURL=chunk-JI6KXOLF.js.map
|
|
920
|
+
//# sourceMappingURL=chunk-JI6KXOLF.js.map
|