agent-relay-server 0.4.22 → 0.4.23
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/package.json +1 -1
- package/public/dashboard.js +730 -9
- package/public/index.html +678 -5
- package/src/db.ts +154 -1
- package/src/routes.ts +105 -0
- package/src/security.ts +2 -1
- package/src/types.ts +23 -0
package/public/dashboard.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const PREF_PREFIX = "ar-";
|
|
3
|
+
const HUMAN_AGENT_ID = "user";
|
|
4
|
+
const INBOX_OPERATOR_ID = HUMAN_AGENT_ID;
|
|
3
5
|
const DEFAULT_COMPOSE = { from: "", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
6
|
+
const DEFAULT_INBOX_COMPOSE = { toMode: "agent", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
7
|
+
const DEFAULT_PAIR_MESSAGE = { pairId: "", from: "", body: "", subject: "" };
|
|
8
|
+
const DEFAULT_PAIR_INVITE = { requesterId: "", targetId: "", objective: "" };
|
|
4
9
|
const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
10
|
+
const WAITING_TASK_STATUSES = new Set(["open", "blocked"]);
|
|
5
11
|
const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
|
|
6
12
|
const LIVE_REFRESH_MS = 5_000;
|
|
7
13
|
const AGENT_TYPE_ICONS = {
|
|
@@ -39,6 +45,7 @@
|
|
|
39
45
|
|
|
40
46
|
agents: [],
|
|
41
47
|
agentsById: {},
|
|
48
|
+
pairs: [],
|
|
42
49
|
messages: [],
|
|
43
50
|
tasks: [],
|
|
44
51
|
taskEvents: [],
|
|
@@ -46,10 +53,20 @@
|
|
|
46
53
|
health: null,
|
|
47
54
|
now: Date.now(),
|
|
48
55
|
authToken: loadPref("authToken", ""),
|
|
56
|
+
inboxReadCursors: loadPref("inboxReadCursors", {}),
|
|
57
|
+
inboxArchivedThreads: loadPref("inboxArchivedThreads", {}),
|
|
58
|
+
inboxDrafts: loadPref("inboxDrafts", {}),
|
|
59
|
+
inboxSearch: "",
|
|
60
|
+
inboxShowArchived: loadPref("inboxShowArchived", false),
|
|
49
61
|
|
|
50
62
|
selectedAgent: "",
|
|
63
|
+
agentDetailOpen: false,
|
|
64
|
+
agentDetailId: "",
|
|
65
|
+
selectedInboxThread: "",
|
|
51
66
|
replyTo: null,
|
|
52
67
|
composeOpen: false,
|
|
68
|
+
pairInviteOpen: false,
|
|
69
|
+
pairMessageOpen: false,
|
|
53
70
|
threadOpen: false,
|
|
54
71
|
threadMessages: [],
|
|
55
72
|
taskEventsOpen: false,
|
|
@@ -57,6 +74,9 @@
|
|
|
57
74
|
authNeeded: false,
|
|
58
75
|
|
|
59
76
|
compose: { ...DEFAULT_COMPOSE },
|
|
77
|
+
pairInvite: { ...DEFAULT_PAIR_INVITE },
|
|
78
|
+
pairMessage: { ...DEFAULT_PAIR_MESSAGE },
|
|
79
|
+
inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
|
|
60
80
|
|
|
61
81
|
confirmModal: { show: false, title: "", message: "", action: null },
|
|
62
82
|
renameModal: { show: false, agentId: "", label: "" },
|
|
@@ -65,6 +85,7 @@
|
|
|
65
85
|
tagFilter: "",
|
|
66
86
|
agentStatusFilter: loadPref("agentStatusFilter", ""),
|
|
67
87
|
agentTagFilter: loadPref("agentTagFilter", ""),
|
|
88
|
+
pairStatusFilter: loadPref("pairStatusFilter", "open"),
|
|
68
89
|
taskStatusFilter: "",
|
|
69
90
|
taskSourceFilter: "",
|
|
70
91
|
|
|
@@ -87,6 +108,8 @@
|
|
|
87
108
|
vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
|
|
88
109
|
vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
|
|
89
110
|
vm.$watch("agentTagFilter", (value) => vm.save("agentTagFilter", value));
|
|
111
|
+
vm.$watch("pairStatusFilter", (value) => vm.save("pairStatusFilter", value));
|
|
112
|
+
vm.$watch("inboxShowArchived", (value) => vm.save("inboxShowArchived", value));
|
|
90
113
|
vm.$watch("view", (value) => {
|
|
91
114
|
vm.save("view", value);
|
|
92
115
|
if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
|
|
@@ -154,9 +177,11 @@
|
|
|
154
177
|
savePref(key, value);
|
|
155
178
|
},
|
|
156
179
|
|
|
157
|
-
switchView(view) {
|
|
180
|
+
async switchView(view) {
|
|
158
181
|
this.view = view;
|
|
159
|
-
if (view === "messages") this.fetchMessages();
|
|
182
|
+
if (view === "inbox" || view === "messages") await this.fetchMessages();
|
|
183
|
+
if (view === "inbox") this.markInboxThreadRead(this.selectedInboxThreadData);
|
|
184
|
+
if (view === "pairs") this.fetchPairs();
|
|
160
185
|
if (view === "tasks") this.fetchTasks();
|
|
161
186
|
},
|
|
162
187
|
|
|
@@ -222,8 +247,8 @@
|
|
|
222
247
|
|
|
223
248
|
function handleNewMessage(vm, msg) {
|
|
224
249
|
if (vm.messages.some((existing) => existing.id === msg.id)) return;
|
|
225
|
-
if (vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
|
|
226
|
-
if (vm.channelFilter && msg.channel !== vm.channelFilter) return;
|
|
250
|
+
if (vm.view === "messages" && vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
|
|
251
|
+
if (vm.view === "messages" && vm.channelFilter && msg.channel !== vm.channelFilter) return;
|
|
227
252
|
|
|
228
253
|
vm.messages.push(msg);
|
|
229
254
|
if (vm.messages.length > 200) vm.messages.shift();
|
|
@@ -299,7 +324,7 @@
|
|
|
299
324
|
},
|
|
300
325
|
|
|
301
326
|
async refresh() {
|
|
302
|
-
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
|
|
327
|
+
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchInboxState()]);
|
|
303
328
|
},
|
|
304
329
|
|
|
305
330
|
async refreshLiveData() {
|
|
@@ -332,11 +357,29 @@
|
|
|
332
357
|
} catch {}
|
|
333
358
|
},
|
|
334
359
|
|
|
360
|
+
async fetchPairs() {
|
|
361
|
+
try {
|
|
362
|
+
let pairs;
|
|
363
|
+
if (this.pairStatusFilter === "open") {
|
|
364
|
+
const [active, pending] = await Promise.all([
|
|
365
|
+
this.api("GET", "/pairs?status=active"),
|
|
366
|
+
this.api("GET", "/pairs?status=pending"),
|
|
367
|
+
]);
|
|
368
|
+
pairs = [...active, ...pending];
|
|
369
|
+
} else if (this.pairStatusFilter) {
|
|
370
|
+
pairs = await this.api("GET", "/pairs?status=" + encodeURIComponent(this.pairStatusFilter));
|
|
371
|
+
} else {
|
|
372
|
+
pairs = await this.api("GET", "/pairs");
|
|
373
|
+
}
|
|
374
|
+
this.pairs = pairs.sort(comparePairs);
|
|
375
|
+
} catch {}
|
|
376
|
+
},
|
|
377
|
+
|
|
335
378
|
async fetchMessages() {
|
|
336
379
|
try {
|
|
337
380
|
let path = "/messages?limit=100";
|
|
338
|
-
if (this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
|
|
339
|
-
if (this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
|
|
381
|
+
if (this.view === "messages" && this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
|
|
382
|
+
if (this.view === "messages" && this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
|
|
340
383
|
this.messages = await this.api("GET", path);
|
|
341
384
|
} catch {}
|
|
342
385
|
},
|
|
@@ -349,13 +392,52 @@
|
|
|
349
392
|
this.tasks = await this.api("GET", "/tasks?" + params.toString());
|
|
350
393
|
} catch {}
|
|
351
394
|
},
|
|
395
|
+
|
|
396
|
+
async fetchInboxState() {
|
|
397
|
+
try {
|
|
398
|
+
const state = await this.api("GET", "/inbox/state?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID));
|
|
399
|
+
applyInboxState(this, state);
|
|
400
|
+
} catch {}
|
|
401
|
+
},
|
|
352
402
|
};
|
|
353
403
|
}
|
|
354
404
|
|
|
405
|
+
function applyInboxState(vm, state) {
|
|
406
|
+
const readCursors = {};
|
|
407
|
+
const archivedThreads = {};
|
|
408
|
+
const drafts = {};
|
|
409
|
+
|
|
410
|
+
for (const thread of state?.threads || []) {
|
|
411
|
+
if (thread.readCursorMessageId) readCursors[thread.peerId] = thread.readCursorMessageId;
|
|
412
|
+
if (thread.archivedAtMessageId) archivedThreads[thread.peerId] = thread.archivedAtMessageId;
|
|
413
|
+
}
|
|
414
|
+
for (const draft of state?.drafts || []) {
|
|
415
|
+
if (draft.body) drafts[draft.peerId] = draft.body;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
vm.inboxReadCursors = readCursors;
|
|
419
|
+
vm.inboxArchivedThreads = archivedThreads;
|
|
420
|
+
vm.inboxDrafts = drafts;
|
|
421
|
+
savePref("inboxReadCursors", readCursors);
|
|
422
|
+
savePref("inboxArchivedThreads", archivedThreads);
|
|
423
|
+
savePref("inboxDrafts", drafts);
|
|
424
|
+
}
|
|
425
|
+
|
|
355
426
|
function createComputedDescriptors() {
|
|
356
427
|
return {
|
|
357
428
|
onlineCount: { get: getOnlineCount },
|
|
358
429
|
sortedAgents: { get: getSortedAgents },
|
|
430
|
+
pairsByAgentId: { get: getPairsByAgentId },
|
|
431
|
+
selectedAgentDetail: { get: getSelectedAgentDetail },
|
|
432
|
+
agentDetailMessages: { get: getAgentDetailMessages },
|
|
433
|
+
pairMessagePair: { get: getPairMessagePair },
|
|
434
|
+
allInboxThreads: { get: getAllInboxThreads },
|
|
435
|
+
inboxThreads: { get: getInboxThreads },
|
|
436
|
+
selectedInboxThreadData: { get: getSelectedInboxThreadData },
|
|
437
|
+
selectedInboxMessages: { get: getSelectedInboxMessages },
|
|
438
|
+
inboxComposeTargetOptions: { get: getInboxComposeTargetOptions },
|
|
439
|
+
attentionSummary: { get: getAttentionSummary },
|
|
440
|
+
attentionAgentCount: { get: getAttentionAgentCount },
|
|
359
441
|
filteredMessages: { get: getFilteredMessages },
|
|
360
442
|
groupedMessages: { get: getGroupedMessages },
|
|
361
443
|
filteredTasks: { get: getFilteredTasks },
|
|
@@ -386,6 +468,210 @@
|
|
|
386
468
|
return list.sort((a, b) => compareAgents(this, a, b) * dir);
|
|
387
469
|
}
|
|
388
470
|
|
|
471
|
+
function getPairsByAgentId() {
|
|
472
|
+
const byAgent = {};
|
|
473
|
+
for (const pair of this.pairs || []) {
|
|
474
|
+
if (pair.requesterId) byAgent[pair.requesterId] = pair;
|
|
475
|
+
if (pair.targetId) byAgent[pair.targetId] = pair;
|
|
476
|
+
}
|
|
477
|
+
return byAgent;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function comparePairs(a, b) {
|
|
481
|
+
return new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function getSelectedAgentDetail() {
|
|
485
|
+
if (!this.agentDetailId) return null;
|
|
486
|
+
return this.agentsById[this.agentDetailId] || null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function getAgentDetailMessages() {
|
|
490
|
+
if (!this.agentDetailId) return [];
|
|
491
|
+
return this.messages
|
|
492
|
+
.filter((msg) => msg.from === this.agentDetailId || msg.to === this.agentDetailId)
|
|
493
|
+
.slice()
|
|
494
|
+
.sort((a, b) => b.id - a.id)
|
|
495
|
+
.slice(0, 8);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getPairMessagePair() {
|
|
499
|
+
return this.pairs.find((pair) => pair.id === this.pairMessage.pairId) || null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function getAllInboxThreads() {
|
|
503
|
+
return buildInboxThreads(this);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getInboxThreads() {
|
|
507
|
+
const search = this.inboxSearch.trim().toLowerCase();
|
|
508
|
+
return this.allInboxThreads.filter((thread) => {
|
|
509
|
+
if (!this.inboxShowArchived && thread.archived) return false;
|
|
510
|
+
if (search && !threadMatchesSearch(this, thread, search)) return false;
|
|
511
|
+
return true;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function buildInboxThreads(vm) {
|
|
516
|
+
const threads = new Map();
|
|
517
|
+
for (const msg of vm.messages) {
|
|
518
|
+
const peer = inboxPeer(msg);
|
|
519
|
+
if (!peer) continue;
|
|
520
|
+
if (!threads.has(peer)) threads.set(peer, { id: peer, peer, messages: [], lastMessage: null });
|
|
521
|
+
threads.get(peer).messages.push(msg);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (const thread of threads.values()) {
|
|
525
|
+
thread.messages.sort((a, b) => a.id - b.id);
|
|
526
|
+
thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
|
|
527
|
+
thread.attention = getThreadAttention(vm, thread);
|
|
528
|
+
thread.archived = isInboxThreadArchived(vm, thread);
|
|
529
|
+
thread.draft = draftForPeer(vm, thread.peer);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return [...threads.values()].sort(compareInboxThreads);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function threadMatchesSearch(vm, thread, search) {
|
|
536
|
+
const haystack = [
|
|
537
|
+
vm.displayTarget(thread.peer),
|
|
538
|
+
thread.peer,
|
|
539
|
+
...thread.messages.flatMap((msg) => [msg.subject || "", msg.body || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
|
|
540
|
+
].join("\n").toLowerCase();
|
|
541
|
+
return haystack.includes(search);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isInboxThreadArchived(vm, thread) {
|
|
545
|
+
const archivedAtId = Number(vm.inboxArchivedThreads?.[thread.peer] || 0);
|
|
546
|
+
return Boolean(thread.lastMessage?.id && archivedAtId >= thread.lastMessage.id);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function compareInboxThreads(a, b) {
|
|
550
|
+
const scoreDelta = (b.attention?.score || 0) - (a.attention?.score || 0);
|
|
551
|
+
if (scoreDelta !== 0) return scoreDelta;
|
|
552
|
+
return (b.lastMessage?.id || 0) - (a.lastMessage?.id || 0);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function getSelectedInboxThreadData() {
|
|
556
|
+
if (!this.selectedInboxThread) return null;
|
|
557
|
+
return this.inboxThreads.find((thread) => thread.id === this.selectedInboxThread) || null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function getSelectedInboxMessages() {
|
|
561
|
+
return this.selectedInboxThreadData?.messages || [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getInboxComposeTargetOptions() {
|
|
565
|
+
if (this.inboxCompose.toMode === "tag") return this.uniqueTags.map((value) => ({ value, label: "#" + value }));
|
|
566
|
+
if (this.inboxCompose.toMode === "cap") return this.uniqueCaps.map((value) => ({ value, label: value }));
|
|
567
|
+
return this.composeAgents.map((agent) => ({ value: agent.id, label: `${this.displayName(agent)} [${agent.id.slice(-6)}]` }));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function getAttentionSummary() {
|
|
571
|
+
const threads = this.allInboxThreads.filter((thread) => !thread.archived);
|
|
572
|
+
const pendingPairInvites = this.pairs.filter((pair) => pair.status === "pending").length;
|
|
573
|
+
const claimableTasks = countClaimableWaiting(this);
|
|
574
|
+
const unreadInbox = threads.reduce((sum, thread) => sum + (thread.attention?.unread || 0), 0);
|
|
575
|
+
const needsHumanResponse = threads.filter((thread) => thread.attention?.needsHumanResponse).length;
|
|
576
|
+
const agentQuestions = threads.filter((thread) => thread.attention?.agentQuestion).length;
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
unreadInbox,
|
|
580
|
+
needsHumanResponse,
|
|
581
|
+
agentQuestions,
|
|
582
|
+
pendingPairInvites,
|
|
583
|
+
claimableTasks,
|
|
584
|
+
total: unreadInbox + needsHumanResponse + agentQuestions + pendingPairInvites + claimableTasks,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function getAttentionAgentCount() {
|
|
589
|
+
return this.sortedAgents.filter((agent) => agentAttention.call(this, agent).total > 0).length;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function inboxPeer(msg) {
|
|
593
|
+
if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to;
|
|
594
|
+
if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from;
|
|
595
|
+
return "";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getThreadAttention(vm, thread) {
|
|
599
|
+
const lastHumanReplyId = maxMessageId(thread.messages, (msg) => msg.from === HUMAN_AGENT_ID);
|
|
600
|
+
const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
|
|
601
|
+
const unread = thread.messages.filter((msg) => isUnreadHumanMessage(vm, thread.peer, msg)).length;
|
|
602
|
+
const needsHumanResponse = lastInboundId > lastHumanReplyId;
|
|
603
|
+
const agentQuestion = thread.messages.some((msg) =>
|
|
604
|
+
isHumanInboundMessage(msg) && msg.id > lastHumanReplyId && messageLooksLikeQuestion(msg)
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
unread,
|
|
609
|
+
needsHumanResponse,
|
|
610
|
+
agentQuestion,
|
|
611
|
+
score: unread * 10 + (needsHumanResponse ? 5 : 0) + (agentQuestion ? 3 : 0),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function maxMessageId(messages, predicate) {
|
|
616
|
+
let max = 0;
|
|
617
|
+
for (const msg of messages) {
|
|
618
|
+
if (predicate(msg) && msg.id > max) max = msg.id;
|
|
619
|
+
}
|
|
620
|
+
return max;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isHumanInboundMessage(msg) {
|
|
624
|
+
return msg.to === HUMAN_AGENT_ID && msg.from !== HUMAN_AGENT_ID;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function isUnreadHumanMessage(vm, peer, msg) {
|
|
628
|
+
if (!isHumanInboundMessage(msg)) return false;
|
|
629
|
+
if ((msg.readBy || []).includes(HUMAN_AGENT_ID)) return false;
|
|
630
|
+
return msg.id > readCursorForPeer(vm, peer);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function readCursorForPeer(vm, peer) {
|
|
634
|
+
const value = Number(vm.inboxReadCursors?.[peer] || 0);
|
|
635
|
+
return Number.isFinite(value) ? value : 0;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function draftForPeer(vm, peer) {
|
|
639
|
+
return typeof vm.inboxDrafts?.[peer] === "string" ? vm.inboxDrafts[peer] : "";
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function messageLooksLikeQuestion(msg) {
|
|
643
|
+
return /\?/.test(`${msg.subject || ""}\n${msg.body || ""}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function countClaimableWaiting(vm) {
|
|
647
|
+
const taskCount = vm.tasks.filter(isClaimableTaskWaiting).length;
|
|
648
|
+
const messageCount = vm.messages.filter(isClaimableMessageWaiting).length;
|
|
649
|
+
return taskCount + messageCount;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function countAgentClaimableWaiting(vm, agent) {
|
|
653
|
+
const taskCount = vm.tasks.filter((task) => isClaimableTaskWaiting(task) && targetMatchesAgent(task.target, agent)).length;
|
|
654
|
+
const messageCount = vm.messages.filter((msg) => isClaimableMessageWaiting(msg) && targetMatchesAgent(msg.to, agent)).length;
|
|
655
|
+
return taskCount + messageCount;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function isClaimableTaskWaiting(task) {
|
|
659
|
+
return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isClaimableMessageWaiting(msg) {
|
|
663
|
+
return Boolean(msg.claimable && !msg.claimedBy);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function targetMatchesAgent(target, agent) {
|
|
667
|
+
if (!target || !agent) return false;
|
|
668
|
+
if (target === "broadcast" || target === agent.id) return true;
|
|
669
|
+
if (target.startsWith("tag:")) return (agent.tags || []).includes(target.slice(4));
|
|
670
|
+
if (target.startsWith("cap:")) return (agent.capabilities || []).includes(target.slice(4));
|
|
671
|
+
if (target.startsWith("label:")) return agent.label === target.slice(6);
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
|
|
389
675
|
function getFilteredMessages() {
|
|
390
676
|
if (!this.tagFilter) return this.messages;
|
|
391
677
|
return this.messages.filter((msg) => messageMatchesTag(this, msg, this.tagFilter));
|
|
@@ -445,6 +731,10 @@
|
|
|
445
731
|
}
|
|
446
732
|
|
|
447
733
|
function compareAgents(vm, a, b) {
|
|
734
|
+
if (vm.agentSort === "status") {
|
|
735
|
+
const attentionDelta = agentAttention.call(vm, b).score - agentAttention.call(vm, a).score;
|
|
736
|
+
if (attentionDelta !== 0) return attentionDelta;
|
|
737
|
+
}
|
|
448
738
|
switch (vm.agentSort) {
|
|
449
739
|
case "name":
|
|
450
740
|
return vm.displayName(a).localeCompare(vm.displayName(b));
|
|
@@ -463,6 +753,16 @@
|
|
|
463
753
|
return {
|
|
464
754
|
displayName,
|
|
465
755
|
displayTarget,
|
|
756
|
+
conversationTitle,
|
|
757
|
+
messagePreview,
|
|
758
|
+
agentPair,
|
|
759
|
+
pairPeerId,
|
|
760
|
+
pairBadgeClass,
|
|
761
|
+
pairStatusClass,
|
|
762
|
+
pairBadgeLabel,
|
|
763
|
+
pairTitle,
|
|
764
|
+
agentAttention,
|
|
765
|
+
agentAttentionTitle,
|
|
466
766
|
agentType,
|
|
467
767
|
agentTypeIcon,
|
|
468
768
|
agentTypeTitle,
|
|
@@ -490,6 +790,103 @@
|
|
|
490
790
|
return agent ? this.displayName(agent) : target.slice(-8);
|
|
491
791
|
}
|
|
492
792
|
|
|
793
|
+
function conversationTitle(thread) {
|
|
794
|
+
if (!thread) return "Inbox";
|
|
795
|
+
return this.displayTarget(thread.peer);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function messagePreview(msg) {
|
|
799
|
+
const text = msg?.subject || msg?.body || "";
|
|
800
|
+
return text.length > 90 ? text.slice(0, 90) + "..." : text;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function agentPair(agent) {
|
|
804
|
+
return agent ? this.pairsByAgentId[agent.id] : null;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function pairPeerId(pair, agentId) {
|
|
808
|
+
if (!pair) return "";
|
|
809
|
+
return pair.requesterId === agentId ? pair.targetId : pair.requesterId;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function pairBadgeClass(pair) {
|
|
813
|
+
if (pair?.status === "active") return "bg-success-lt";
|
|
814
|
+
if (pair?.status === "pending") return "bg-warning-lt";
|
|
815
|
+
return "bg-secondary-lt";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function pairStatusClass(pair) {
|
|
819
|
+
if (pair?.status === "active") return "bg-success";
|
|
820
|
+
if (pair?.status === "pending") return "bg-warning";
|
|
821
|
+
if (pair?.status === "rejected" || pair?.status === "expired") return "bg-danger";
|
|
822
|
+
return "bg-secondary";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function pairBadgeLabel(pair, agentId) {
|
|
826
|
+
if (!pair) return "";
|
|
827
|
+
const peer = this.displayTarget(pairPeerId(pair, agentId));
|
|
828
|
+
if (pair.status === "active") return "paired with " + peer;
|
|
829
|
+
if (pair.status === "pending" && pair.requesterId === agentId) return "invite to " + peer;
|
|
830
|
+
if (pair.status === "pending") return "invite from " + peer;
|
|
831
|
+
return pair.status;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function pairTitle(pair, agentId) {
|
|
835
|
+
if (!pair) return "";
|
|
836
|
+
const label = pairBadgeLabel.call(this, pair, agentId);
|
|
837
|
+
const objective = pair.objective ? " - " + pair.objective : "";
|
|
838
|
+
return `${label} (${pair.id})${objective}`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function agentAttention(agent) {
|
|
842
|
+
if (!agent) return emptyAttention();
|
|
843
|
+
const thread = this.allInboxThreads.find((item) => item.peer === agent.id && !item.archived);
|
|
844
|
+
const pair = this.agentPair(agent);
|
|
845
|
+
const pendingPairInvite = pair?.status === "pending";
|
|
846
|
+
const claimableTasks = countAgentClaimableWaiting(this, agent);
|
|
847
|
+
const attention = {
|
|
848
|
+
unread: thread?.attention?.unread || 0,
|
|
849
|
+
needsHumanResponse: Boolean(thread?.attention?.needsHumanResponse),
|
|
850
|
+
agentQuestion: Boolean(thread?.attention?.agentQuestion),
|
|
851
|
+
pendingPairInvite,
|
|
852
|
+
claimableTasks,
|
|
853
|
+
};
|
|
854
|
+
attention.total = attention.unread +
|
|
855
|
+
(attention.needsHumanResponse ? 1 : 0) +
|
|
856
|
+
(attention.agentQuestion ? 1 : 0) +
|
|
857
|
+
(attention.pendingPairInvite ? 1 : 0) +
|
|
858
|
+
attention.claimableTasks;
|
|
859
|
+
attention.score = attention.unread * 10 +
|
|
860
|
+
(attention.needsHumanResponse ? 5 : 0) +
|
|
861
|
+
(attention.agentQuestion ? 3 : 0) +
|
|
862
|
+
(attention.pendingPairInvite ? 4 : 0) +
|
|
863
|
+
attention.claimableTasks * 2;
|
|
864
|
+
return attention;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function emptyAttention() {
|
|
868
|
+
return {
|
|
869
|
+
unread: 0,
|
|
870
|
+
needsHumanResponse: false,
|
|
871
|
+
agentQuestion: false,
|
|
872
|
+
pendingPairInvite: false,
|
|
873
|
+
claimableTasks: 0,
|
|
874
|
+
total: 0,
|
|
875
|
+
score: 0,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function agentAttentionTitle(agent) {
|
|
880
|
+
const attention = agentAttention.call(this, agent);
|
|
881
|
+
const parts = [];
|
|
882
|
+
if (attention.unread) parts.push(`${attention.unread} unread`);
|
|
883
|
+
if (attention.needsHumanResponse) parts.push("needs human response");
|
|
884
|
+
if (attention.agentQuestion) parts.push("agent asked a question");
|
|
885
|
+
if (attention.pendingPairInvite) parts.push("pair invite pending");
|
|
886
|
+
if (attention.claimableTasks) parts.push(`${attention.claimableTasks} claimable waiting`);
|
|
887
|
+
return parts.join(", ");
|
|
888
|
+
}
|
|
889
|
+
|
|
493
890
|
function agentType(agent) {
|
|
494
891
|
const values = [
|
|
495
892
|
...(agent?.tags || []),
|
|
@@ -561,6 +958,20 @@
|
|
|
561
958
|
return {
|
|
562
959
|
openCompose,
|
|
563
960
|
openComposeToAgent,
|
|
961
|
+
openComposeToInboxThread,
|
|
962
|
+
openInboxThread,
|
|
963
|
+
markInboxThreadRead,
|
|
964
|
+
markInboxThreadUnread,
|
|
965
|
+
archiveInboxThread,
|
|
966
|
+
unarchiveInboxThread,
|
|
967
|
+
confirmDeleteInboxThread,
|
|
968
|
+
doDeleteInboxThread,
|
|
969
|
+
replyDraftForThread,
|
|
970
|
+
setReplyDraft,
|
|
971
|
+
clearReplyDraft,
|
|
972
|
+
sendInboxReply,
|
|
973
|
+
resetInboxComposeTarget,
|
|
974
|
+
doSendInboxCompose,
|
|
564
975
|
startReply,
|
|
565
976
|
cancelReply,
|
|
566
977
|
doSend,
|
|
@@ -571,6 +982,20 @@
|
|
|
571
982
|
};
|
|
572
983
|
}
|
|
573
984
|
|
|
985
|
+
function createPairActions() {
|
|
986
|
+
return {
|
|
987
|
+
openPairMessage,
|
|
988
|
+
closePairMessage,
|
|
989
|
+
openPairInvite,
|
|
990
|
+
closePairInvite,
|
|
991
|
+
doCreatePair,
|
|
992
|
+
doSendPairMessage,
|
|
993
|
+
doAcceptPair,
|
|
994
|
+
doRejectPair,
|
|
995
|
+
doHangupPair,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
574
999
|
function focusComposeBody(vm) {
|
|
575
1000
|
vm.$nextTick(() => vm.$refs.composeBody?.focus());
|
|
576
1001
|
}
|
|
@@ -588,12 +1013,195 @@
|
|
|
588
1013
|
focusComposeBody(this);
|
|
589
1014
|
}
|
|
590
1015
|
|
|
1016
|
+
function openComposeToInboxThread(thread) {
|
|
1017
|
+
if (!thread) return;
|
|
1018
|
+
this.replyTo = thread.lastMessage ? { id: thread.lastMessage.id, from: thread.lastMessage.from } : null;
|
|
1019
|
+
this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID, to: thread.peer, channel: thread.lastMessage?.channel || "" };
|
|
1020
|
+
this.composeOpen = true;
|
|
1021
|
+
focusComposeBody(this);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function openInboxThread(thread) {
|
|
1025
|
+
this.selectedInboxThread = thread?.id || "";
|
|
1026
|
+
this.markInboxThreadRead(thread);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function markInboxThreadRead(thread) {
|
|
1030
|
+
if (!thread?.peer || !thread.messages?.length) return;
|
|
1031
|
+
const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
|
|
1032
|
+
if (lastInboundId <= readCursorForPeer(this, thread.peer)) return;
|
|
1033
|
+
this.inboxReadCursors = { ...this.inboxReadCursors, [thread.peer]: lastInboundId };
|
|
1034
|
+
savePref("inboxReadCursors", this.inboxReadCursors);
|
|
1035
|
+
void saveInboxThreadState(this, {
|
|
1036
|
+
peerId: thread.peer,
|
|
1037
|
+
readCursorMessageId: lastInboundId,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function markInboxThreadUnread(thread) {
|
|
1042
|
+
if (!thread?.peer) return;
|
|
1043
|
+
const next = { ...this.inboxReadCursors };
|
|
1044
|
+
delete next[thread.peer];
|
|
1045
|
+
this.inboxReadCursors = next;
|
|
1046
|
+
savePref("inboxReadCursors", this.inboxReadCursors);
|
|
1047
|
+
void saveInboxThreadState(this, {
|
|
1048
|
+
peerId: thread.peer,
|
|
1049
|
+
readCursorMessageId: null,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function archiveInboxThread(thread) {
|
|
1054
|
+
if (!thread?.peer || !thread.lastMessage) return;
|
|
1055
|
+
this.inboxArchivedThreads = { ...this.inboxArchivedThreads, [thread.peer]: thread.lastMessage.id };
|
|
1056
|
+
savePref("inboxArchivedThreads", this.inboxArchivedThreads);
|
|
1057
|
+
void saveInboxThreadState(this, {
|
|
1058
|
+
peerId: thread.peer,
|
|
1059
|
+
archivedAtMessageId: thread.lastMessage.id,
|
|
1060
|
+
});
|
|
1061
|
+
if (this.selectedInboxThread === thread.id) this.selectedInboxThread = "";
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function unarchiveInboxThread(thread) {
|
|
1065
|
+
if (!thread?.peer) return;
|
|
1066
|
+
const next = { ...this.inboxArchivedThreads };
|
|
1067
|
+
delete next[thread.peer];
|
|
1068
|
+
this.inboxArchivedThreads = next;
|
|
1069
|
+
savePref("inboxArchivedThreads", this.inboxArchivedThreads);
|
|
1070
|
+
void saveInboxThreadState(this, {
|
|
1071
|
+
peerId: thread.peer,
|
|
1072
|
+
archivedAtMessageId: null,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function saveInboxThreadState(vm, patch) {
|
|
1077
|
+
try {
|
|
1078
|
+
await vm.api("PATCH", "/inbox/threads", { operatorId: INBOX_OPERATOR_ID, ...patch });
|
|
1079
|
+
} catch {}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function confirmDeleteInboxThread(thread) {
|
|
1083
|
+
if (!thread) return;
|
|
1084
|
+
this.openConfirm(
|
|
1085
|
+
"Delete Thread",
|
|
1086
|
+
`Delete ${thread.messages.length} message(s) in ${this.conversationTitle(thread)}? This cannot be undone.`,
|
|
1087
|
+
() => this.doDeleteInboxThread(thread)
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function doDeleteInboxThread(thread) {
|
|
1092
|
+
if (!thread?.messages?.length) return;
|
|
1093
|
+
try {
|
|
1094
|
+
await Promise.all(thread.messages.map((msg) => this.api("DELETE", "/messages/" + msg.id)));
|
|
1095
|
+
this.messages = this.messages.filter((msg) => !thread.messages.some((item) => item.id === msg.id));
|
|
1096
|
+
this.selectedInboxThread = "";
|
|
1097
|
+
this.clearReplyDraft(thread);
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
alert("Delete thread failed: " + e.message);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function replyDraftForThread(thread) {
|
|
1104
|
+
return thread?.peer ? draftForPeer(this, thread.peer) : "";
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function setReplyDraft(thread, value) {
|
|
1108
|
+
if (!thread?.peer) return;
|
|
1109
|
+
const next = { ...this.inboxDrafts, [thread.peer]: value };
|
|
1110
|
+
if (!value) delete next[thread.peer];
|
|
1111
|
+
this.inboxDrafts = next;
|
|
1112
|
+
savePref("inboxDrafts", this.inboxDrafts);
|
|
1113
|
+
if (value) {
|
|
1114
|
+
void saveInboxDraft(this, thread.peer, value);
|
|
1115
|
+
} else {
|
|
1116
|
+
void deleteInboxDraftState(this, thread.peer);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function clearReplyDraft(thread) {
|
|
1121
|
+
if (!thread?.peer) return;
|
|
1122
|
+
this.setReplyDraft(thread, "");
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function saveInboxDraft(vm, peerId, body) {
|
|
1126
|
+
try {
|
|
1127
|
+
await vm.api("PUT", "/inbox/drafts", { operatorId: INBOX_OPERATOR_ID, peerId, body });
|
|
1128
|
+
} catch {}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async function deleteInboxDraftState(vm, peerId) {
|
|
1132
|
+
try {
|
|
1133
|
+
await vm.api("DELETE", "/inbox/drafts?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID) + "&peerId=" + encodeURIComponent(peerId));
|
|
1134
|
+
} catch {}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async function sendInboxReply(thread) {
|
|
1138
|
+
if (!thread) return;
|
|
1139
|
+
const body = this.replyDraftForThread(thread).trim();
|
|
1140
|
+
if (!body) {
|
|
1141
|
+
alert("Reply body is required.");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
const payload = {
|
|
1147
|
+
from: HUMAN_AGENT_ID,
|
|
1148
|
+
to: thread.peer,
|
|
1149
|
+
body,
|
|
1150
|
+
};
|
|
1151
|
+
if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
|
|
1152
|
+
if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
|
|
1153
|
+
await this.api("POST", "/messages", payload);
|
|
1154
|
+
this.clearReplyDraft(thread);
|
|
1155
|
+
this.markInboxThreadRead(thread);
|
|
1156
|
+
await this.fetchMessages();
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
alert("Reply failed: " + e.message);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function resetInboxComposeTarget() {
|
|
1163
|
+
this.inboxCompose = { ...this.inboxCompose, to: "" };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function inboxComposeTarget(vm) {
|
|
1167
|
+
const target = vm.inboxCompose.to;
|
|
1168
|
+
if (!target) return "";
|
|
1169
|
+
if (vm.inboxCompose.toMode === "tag") return "tag:" + target;
|
|
1170
|
+
if (vm.inboxCompose.toMode === "cap") return "cap:" + target;
|
|
1171
|
+
return target;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async function doSendInboxCompose() {
|
|
1175
|
+
const target = inboxComposeTarget(this);
|
|
1176
|
+
if (!target || !this.inboxCompose.body) {
|
|
1177
|
+
alert("Target and Message are required.");
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
const payload = {
|
|
1183
|
+
from: HUMAN_AGENT_ID,
|
|
1184
|
+
to: target,
|
|
1185
|
+
body: this.inboxCompose.body,
|
|
1186
|
+
};
|
|
1187
|
+
if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
|
|
1188
|
+
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
1189
|
+
if (this.inboxCompose.claimable) payload.claimable = true;
|
|
1190
|
+
await this.api("POST", "/messages", payload);
|
|
1191
|
+
this.inboxCompose = { ...DEFAULT_INBOX_COMPOSE, toMode: this.inboxCompose.toMode, to: this.inboxCompose.to };
|
|
1192
|
+
await this.fetchMessages();
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
alert("Send failed: " + e.message);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
591
1198
|
function startReply(msg) {
|
|
1199
|
+
const replyTarget = msg.from === HUMAN_AGENT_ID ? msg.to : msg.from;
|
|
592
1200
|
this.replyTo = { id: msg.id, from: msg.from };
|
|
593
1201
|
this.compose = {
|
|
594
1202
|
...DEFAULT_COMPOSE,
|
|
595
|
-
from: "",
|
|
596
|
-
to:
|
|
1203
|
+
from: this.view === "inbox" ? HUMAN_AGENT_ID : "",
|
|
1204
|
+
to: replyTarget,
|
|
597
1205
|
channel: msg.channel || "",
|
|
598
1206
|
};
|
|
599
1207
|
this.openCompose();
|
|
@@ -627,6 +1235,7 @@
|
|
|
627
1235
|
this.composeOpen = false;
|
|
628
1236
|
this.replyTo = null;
|
|
629
1237
|
this.compose = { ...DEFAULT_COMPOSE };
|
|
1238
|
+
await this.fetchMessages();
|
|
630
1239
|
} catch (e) {
|
|
631
1240
|
alert("Send failed: " + e.message);
|
|
632
1241
|
}
|
|
@@ -676,8 +1285,119 @@
|
|
|
676
1285
|
}
|
|
677
1286
|
}
|
|
678
1287
|
|
|
1288
|
+
function openPairMessage(pair, fromId) {
|
|
1289
|
+
if (!pair) return;
|
|
1290
|
+
this.pairMessage = {
|
|
1291
|
+
...DEFAULT_PAIR_MESSAGE,
|
|
1292
|
+
pairId: pair.id,
|
|
1293
|
+
from: fromId || pair.requesterId || pair.targetId || "",
|
|
1294
|
+
};
|
|
1295
|
+
this.pairMessageOpen = true;
|
|
1296
|
+
this.$nextTick(() => this.$refs?.pairMessageBody?.focus());
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function openPairInvite(requesterId) {
|
|
1300
|
+
this.pairInvite = {
|
|
1301
|
+
...DEFAULT_PAIR_INVITE,
|
|
1302
|
+
requesterId: requesterId || this.selectedAgent || "",
|
|
1303
|
+
};
|
|
1304
|
+
this.pairInviteOpen = true;
|
|
1305
|
+
this.$nextTick(() => this.$refs?.pairInviteObjective?.focus());
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function closePairInvite() {
|
|
1309
|
+
this.pairInviteOpen = false;
|
|
1310
|
+
this.pairInvite = { ...DEFAULT_PAIR_INVITE };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function doCreatePair() {
|
|
1314
|
+
if (!this.pairInvite.requesterId || !this.pairInvite.targetId) {
|
|
1315
|
+
alert("Requester and Target are required.");
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (this.pairInvite.requesterId === this.pairInvite.targetId) {
|
|
1319
|
+
alert("Requester and Target must be different agents.");
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
const payload = {
|
|
1325
|
+
requesterId: this.pairInvite.requesterId,
|
|
1326
|
+
targetId: this.pairInvite.targetId,
|
|
1327
|
+
};
|
|
1328
|
+
if (this.pairInvite.objective) payload.objective = this.pairInvite.objective;
|
|
1329
|
+
await this.api("POST", "/pairs", payload);
|
|
1330
|
+
this.closePairInvite();
|
|
1331
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
alert("Pair invite failed: " + e.message);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function closePairMessage() {
|
|
1338
|
+
this.pairMessageOpen = false;
|
|
1339
|
+
this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
async function doSendPairMessage() {
|
|
1343
|
+
if (!this.pairMessage.pairId || !this.pairMessage.from || !this.pairMessage.body) {
|
|
1344
|
+
alert("Pair, From, and Message are required.");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
try {
|
|
1349
|
+
const payload = { from: this.pairMessage.from, body: this.pairMessage.body };
|
|
1350
|
+
if (this.pairMessage.subject) payload.subject = this.pairMessage.subject;
|
|
1351
|
+
await this.api("POST", "/pairs/" + encodeURIComponent(this.pairMessage.pairId) + "/messages", payload);
|
|
1352
|
+
this.closePairMessage();
|
|
1353
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
alert("Pair message failed: " + e.message);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
async function doAcceptPair(pair) {
|
|
1360
|
+
if (!pair) return;
|
|
1361
|
+
try {
|
|
1362
|
+
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/accept", { agentId: pair.targetId });
|
|
1363
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
alert("Accept failed: " + e.message);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function doRejectPair(pair) {
|
|
1370
|
+
if (!pair) return;
|
|
1371
|
+
try {
|
|
1372
|
+
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/reject", { agentId: pair.targetId });
|
|
1373
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
alert("Reject failed: " + e.message);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
async function doHangupPair(pair, agentId) {
|
|
1380
|
+
if (!pair) return;
|
|
1381
|
+
try {
|
|
1382
|
+
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/hangup", { agentId: agentId || pair.requesterId });
|
|
1383
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1384
|
+
} catch (e) {
|
|
1385
|
+
alert("Hang up failed: " + e.message);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
679
1389
|
function createAgentActions() {
|
|
680
1390
|
return {
|
|
1391
|
+
openAgentDetail(agent) {
|
|
1392
|
+
if (!agent) return;
|
|
1393
|
+
this.agentDetailId = agent.id;
|
|
1394
|
+
this.agentDetailOpen = true;
|
|
1395
|
+
},
|
|
1396
|
+
|
|
1397
|
+
closeAgentDetail() {
|
|
1398
|
+
this.agentDetailOpen = false;
|
|
1399
|
+
},
|
|
1400
|
+
|
|
681
1401
|
openRename(agent) {
|
|
682
1402
|
this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
|
|
683
1403
|
this.$nextTick(() => this.$refs.renameInput?.focus());
|
|
@@ -826,6 +1546,7 @@
|
|
|
826
1546
|
...createApiMethods(),
|
|
827
1547
|
...createDisplayMethods(),
|
|
828
1548
|
...createMessageActions(),
|
|
1549
|
+
...createPairActions(),
|
|
829
1550
|
...createAgentActions(),
|
|
830
1551
|
...createChartMethods(),
|
|
831
1552
|
};
|