@escalated-dev/escalated 0.7.0 → 0.7.1
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/LICENSE +21 -21
- package/README.md +31 -0
- package/package.json +12 -6
- package/src/components/ActiveChatsPanel.vue +246 -0
- package/src/components/ActivityTimeline.stories.js +55 -55
- package/src/components/ActivityTimeline.vue +83 -83
- package/src/components/AdminDashboard.stories.js +1423 -1095
- package/src/components/AgentLoadIndicator.stories.js +37 -37
- package/src/components/AgentLoadIndicator.vue +49 -49
- package/src/components/AgentPerformanceCard.vue +44 -44
- package/src/components/ArticleEditor.vue +82 -82
- package/src/components/AssigneeSelect.vue +37 -37
- package/src/components/AttachmentList.vue +13 -1
- package/src/components/AuditLogEntry.vue +95 -95
- package/src/components/BulkActionBar.vue +166 -166
- package/src/components/CategoryTree.vue +89 -89
- package/src/components/ChartWidget.vue +46 -46
- package/src/components/ChatActionBar.vue +226 -0
- package/src/components/ChatBubble.vue +162 -0
- package/src/components/ChatComposer.vue +308 -0
- package/src/components/ChatQueue.vue +222 -0
- package/src/components/ChatThread.vue +197 -0
- package/src/components/CollisionWarning.stories.js +30 -30
- package/src/components/CollisionWarning.vue +68 -68
- package/src/components/ConditionalFieldRules.vue +135 -135
- package/src/components/ContextPanel.vue +66 -66
- package/src/components/ContextPanelSection.vue +65 -65
- package/src/components/CustomFieldRenderer.vue +159 -159
- package/src/components/EscalatedLayout.vue +29 -6
- package/src/components/FileDropzone.vue +57 -57
- package/src/components/FollowButton.stories.js +30 -30
- package/src/components/FollowButton.vue +88 -88
- package/src/components/KeyboardShortcutHelp.vue +213 -213
- package/src/components/KnowledgePanel.vue +122 -122
- package/src/components/KpiCard.stories.js +32 -32
- package/src/components/KpiCard.vue +88 -88
- package/src/components/MacroDropdown.vue +150 -150
- package/src/components/MentionDropdown.vue +154 -0
- package/src/components/MetricComparison.vue +89 -0
- package/src/components/PercentileChart.vue +67 -0
- package/src/components/PeriodSelector.vue +98 -0
- package/src/components/PermissionMatrix.vue +107 -107
- package/src/components/PinnedNotes.vue +129 -129
- package/src/components/PluginSlot.vue +68 -62
- package/src/components/PresenceIndicator.vue +46 -2
- package/src/components/PriorityBadge.stories.js +33 -33
- package/src/components/PriorityBadge.vue +46 -46
- package/src/components/ReplyComposer.vue +81 -18
- package/src/components/ReplyThread.vue +40 -2
- package/src/components/ReportChart.vue +395 -0
- package/src/components/ReportExportButton.vue +108 -0
- package/src/components/SatisfactionRating.vue +180 -180
- package/src/components/SaveViewDialog.vue +172 -0
- package/src/components/SavedViewSidebar.vue +208 -0
- package/src/components/ScheduleEditor.vue +104 -104
- package/src/components/SideConversation.vue +165 -165
- package/src/components/SideConversationList.vue +147 -147
- package/src/components/SkillTagManager.vue +79 -79
- package/src/components/SlaComplianceChart.vue +46 -53
- package/src/components/SlaTimer.stories.js +35 -35
- package/src/components/SlaTimer.vue +62 -62
- package/src/components/SnoozeButton.vue +247 -0
- package/src/components/StatsCard.stories.js +24 -24
- package/src/components/StatsCard.vue +58 -58
- package/src/components/StatusBadge.stories.js +53 -53
- package/src/components/StatusBadge.vue +78 -78
- package/src/components/TagSelect.vue +84 -84
- package/src/components/TicketFilters.vue +270 -225
- package/src/components/TicketLinkPanel.vue +193 -193
- package/src/components/TicketList.vue +655 -594
- package/src/components/TicketMergeDialog.vue +157 -157
- package/src/components/TicketSplitDialog.vue +98 -0
- package/src/components/TotpInput.stories.js +9 -9
- package/src/components/TotpInput.vue +81 -81
- package/src/components/TwoFactorSetup.vue +113 -113
- package/src/components/VariableTokenInput.vue +111 -0
- package/src/components/WorkflowActionConfig.vue +287 -0
- package/src/components/WorkflowActionList.vue +350 -0
- package/src/components/WorkflowConditionBuilder.vue +265 -0
- package/src/components/WorkflowTriggerSelector.vue +125 -0
- package/src/composables/useChat.js +187 -0
- package/src/composables/useI18n.js +80 -80
- package/src/composables/useKeyboardShortcuts.js +46 -46
- package/src/composables/useMentions.js +134 -0
- package/src/composables/usePluginExtensions.js +159 -159
- package/src/composables/usePluginHooks.js +65 -65
- package/src/composables/useRealtime.js +182 -0
- package/src/defineEscalatedPlugin.js +76 -76
- package/src/index.js +84 -72
- package/src/locales/ar.json +727 -0
- package/src/locales/de.json +678 -430
- package/src/locales/en.json +717 -455
- package/src/locales/es.json +679 -430
- package/src/locales/fr.json +680 -431
- package/src/locales/index.js +12 -2
- package/src/locales/it.json +727 -0
- package/src/locales/ja.json +727 -0
- package/src/locales/ko.json +727 -0
- package/src/locales/nl.json +727 -0
- package/src/locales/pl.json +727 -0
- package/src/locales/pt-BR.json +727 -0
- package/src/locales/ru.json +727 -0
- package/src/locales/tr.json +727 -0
- package/src/locales/zh-CN.json +727 -0
- package/src/pages/Admin/ApiTokens/Index.vue +381 -381
- package/src/pages/Admin/AuditLog/Index.vue +154 -154
- package/src/pages/Admin/BusinessHours/Form.vue +172 -172
- package/src/pages/Admin/BusinessHours/Index.vue +113 -113
- package/src/pages/Admin/CannedResponses/Index.vue +127 -127
- package/src/pages/Admin/Capacity/Index.vue +134 -134
- package/src/pages/Admin/CustomFields/Form.vue +195 -195
- package/src/pages/Admin/CustomFields/Index.vue +134 -134
- package/src/pages/Admin/CustomObjects/Form.vue +202 -202
- package/src/pages/Admin/CustomObjects/Index.vue +127 -127
- package/src/pages/Admin/CustomObjects/Records.vue +228 -228
- package/src/pages/Admin/Departments/Form.vue +75 -75
- package/src/pages/Admin/Departments/Index.vue +110 -109
- package/src/pages/Admin/EscalationRules/Form.vue +157 -157
- package/src/pages/Admin/EscalationRules/Index.vue +178 -178
- package/src/pages/Admin/Import/Connect.vue +0 -1
- package/src/pages/Admin/Import/Index.vue +1 -1
- package/src/pages/Admin/Import/Progress.vue +1 -1
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
- package/src/pages/Admin/Macros/Index.vue +448 -448
- package/src/pages/Admin/Plugins/Index.vue +327 -327
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
- package/src/pages/Admin/Reports/Cohorts.vue +180 -0
- package/src/pages/Admin/Reports/Comparison.vue +141 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -127
- package/src/pages/Admin/Reports/Dashboard.vue +36 -21
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
- package/src/pages/Admin/Reports/SlaReport.vue +162 -166
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
- package/src/pages/Admin/Reports.vue +206 -206
- package/src/pages/Admin/Roles/Form.vue +71 -71
- package/src/pages/Admin/Roles/Index.vue +122 -122
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
- package/src/pages/Admin/Settings/DataRetention.vue +182 -182
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
- package/src/pages/Admin/Settings/Sandbox.vue +135 -135
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
- package/src/pages/Admin/Settings.vue +1008 -468
- package/src/pages/Admin/Skills/Form.vue +53 -53
- package/src/pages/Admin/Skills/Index.vue +94 -94
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
- package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
- package/src/pages/Admin/Statuses/Form.vue +114 -114
- package/src/pages/Admin/Statuses/Index.vue +153 -153
- package/src/pages/Admin/Tags/Index.vue +191 -191
- package/src/pages/Admin/Tickets/Index.vue +126 -91
- package/src/pages/Admin/Tickets/Show.vue +29 -0
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
- package/src/pages/Admin/Webhooks/Form.vue +110 -110
- package/src/pages/Admin/Webhooks/Index.vue +136 -136
- package/src/pages/Admin/Workflows/Builder.vue +207 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -0
- package/src/pages/Admin/Workflows/Logs.vue +286 -0
- package/src/pages/Agent/TicketIndex.vue +112 -88
- package/src/pages/Agent/TicketShow.vue +192 -60
- package/src/pages/Customer/Create.vue +1 -1
- package/src/pages/Customer/Index.vue +31 -31
- package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
- package/src/pages/Customer/Show.vue +69 -63
- package/src/pages/Guest/Create.vue +40 -15
- package/src/pages/Guest/Show.vue +124 -93
- package/src/plugin.js +25 -0
- package/src/utils/mentionHighlight.js +24 -0
- package/src/utils/sanitizeHtml.js +78 -64
- package/src/widget/EscalatedWidget.vue +804 -0
- package/src/widget/index.js +254 -0
- package/src/pages/Admin/Automations/Form.vue +0 -184
- package/src/pages/Admin/Automations/Index.vue +0 -129
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
modelValue: { type: String, default: '' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits(['update:modelValue']);
|
|
9
|
+
|
|
10
|
+
const triggers = [
|
|
11
|
+
{
|
|
12
|
+
value: 'ticket_created',
|
|
13
|
+
label: 'Ticket Created',
|
|
14
|
+
description: 'When a new ticket is submitted',
|
|
15
|
+
icon: 'M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
value: 'ticket_updated',
|
|
19
|
+
label: 'Ticket Updated',
|
|
20
|
+
description: 'When any ticket field is modified',
|
|
21
|
+
icon: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: 'reply_added',
|
|
25
|
+
label: 'Reply Added',
|
|
26
|
+
description: 'When a reply is posted on a ticket',
|
|
27
|
+
icon: 'M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: 'status_changed',
|
|
31
|
+
label: 'Status Changed',
|
|
32
|
+
description: 'When the ticket status is updated',
|
|
33
|
+
icon: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: 'ticket_assigned',
|
|
37
|
+
label: 'Ticket Assigned',
|
|
38
|
+
description: 'When a ticket is assigned to an agent',
|
|
39
|
+
icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: 'ticket_escalated',
|
|
43
|
+
label: 'Ticket Escalated',
|
|
44
|
+
description: 'When a ticket is escalated',
|
|
45
|
+
icon: 'M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
value: 'sla_breached',
|
|
49
|
+
label: 'SLA Breached',
|
|
50
|
+
description: 'When an SLA target is breached',
|
|
51
|
+
icon: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
value: 'sla_warning',
|
|
55
|
+
label: 'SLA Warning',
|
|
56
|
+
description: 'When an SLA target is approaching',
|
|
57
|
+
icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'chat_started',
|
|
61
|
+
label: 'Chat Started',
|
|
62
|
+
description: 'When a live chat session begins',
|
|
63
|
+
icon: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: 'chat_ended',
|
|
67
|
+
label: 'Chat Ended',
|
|
68
|
+
description: 'When a live chat session ends',
|
|
69
|
+
icon: 'M15 11.25l-3-3m0 0l-3 3m3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const selected = computed(() => props.modelValue);
|
|
74
|
+
|
|
75
|
+
function select(value) {
|
|
76
|
+
emit('update:modelValue', value);
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
|
82
|
+
<button
|
|
83
|
+
v-for="trigger in triggers"
|
|
84
|
+
:key="trigger.value"
|
|
85
|
+
type="button"
|
|
86
|
+
class="group relative flex flex-col items-center rounded-xl border-2 p-4 text-center transition-all"
|
|
87
|
+
:class="
|
|
88
|
+
selected === trigger.value
|
|
89
|
+
? 'border-blue-500 bg-blue-500/10 shadow-lg shadow-blue-500/10'
|
|
90
|
+
: 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] hover:border-[var(--esc-panel-text-muted)] hover:bg-[var(--esc-panel-hover)]'
|
|
91
|
+
"
|
|
92
|
+
@click="select(trigger.value)"
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
class="mb-2 flex h-10 w-10 items-center justify-center rounded-lg"
|
|
96
|
+
:class="
|
|
97
|
+
selected === trigger.value
|
|
98
|
+
? 'bg-blue-500/20 text-blue-400'
|
|
99
|
+
: 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] group-hover:text-[var(--esc-panel-text-secondary)]'
|
|
100
|
+
"
|
|
101
|
+
>
|
|
102
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
103
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="trigger.icon" />
|
|
104
|
+
</svg>
|
|
105
|
+
</div>
|
|
106
|
+
<span
|
|
107
|
+
class="text-xs font-semibold"
|
|
108
|
+
:class="selected === trigger.value ? 'text-blue-400' : 'text-[var(--esc-panel-text-secondary)]'"
|
|
109
|
+
>
|
|
110
|
+
{{ trigger.label }}
|
|
111
|
+
</span>
|
|
112
|
+
<span class="mt-1 text-[10px] leading-tight text-[var(--esc-panel-text-muted)]">
|
|
113
|
+
{{ trigger.description }}
|
|
114
|
+
</span>
|
|
115
|
+
<div
|
|
116
|
+
v-if="selected === trigger.value"
|
|
117
|
+
class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white"
|
|
118
|
+
>
|
|
119
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
|
120
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
121
|
+
</svg>
|
|
122
|
+
</div>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ref, readonly } from 'vue';
|
|
2
|
+
import { useRealtime } from './useRealtime';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composable for live chat functionality.
|
|
6
|
+
*
|
|
7
|
+
* Provides methods to start, send messages, end, and rate chat sessions,
|
|
8
|
+
* plus real-time subscription via useRealtime.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} [options]
|
|
11
|
+
* @param {string} [options.widgetPath] - Path prefix under which the host
|
|
12
|
+
* framework mounts widget routes. Defaults to `/support/widget` which
|
|
13
|
+
* matches every host-adapter plugin (Laravel / Rails / Django / Adonis /
|
|
14
|
+
* WordPress / Filament / Symfony / .NET / Go / Spring / Phoenix). On a
|
|
15
|
+
* NestJS backend, pass `/escalated/widget` (or read from whatever
|
|
16
|
+
* configuration channel the host app exposes to the client).
|
|
17
|
+
*/
|
|
18
|
+
export function useChat(options = {}) {
|
|
19
|
+
const widgetPath = options.widgetPath ?? '/support/widget';
|
|
20
|
+
const connectionState = ref('disconnected'); // disconnected | connecting | connected
|
|
21
|
+
const messageBuffer = ref([]);
|
|
22
|
+
|
|
23
|
+
const { echoAvailable, listen } = useRealtime();
|
|
24
|
+
|
|
25
|
+
function csrfToken() {
|
|
26
|
+
return typeof document !== 'undefined' ? document.querySelector('meta[name="csrf-token"]')?.content || '' : '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function apiRequest(method, path, body = null) {
|
|
30
|
+
const opts = {
|
|
31
|
+
method,
|
|
32
|
+
headers: {
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
36
|
+
'X-CSRF-TOKEN': csrfToken(),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
if (body) opts.body = JSON.stringify(body);
|
|
40
|
+
const res = await fetch(path, opts);
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
if (!res.ok) throw { status: res.status, data };
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Start a new chat session.
|
|
48
|
+
* @param {Object} data - { name, email, department_id, message }
|
|
49
|
+
* @returns {Promise<Object>} The created chat session
|
|
50
|
+
*/
|
|
51
|
+
async function startChat(data) {
|
|
52
|
+
return apiRequest('POST', `${widgetPath}/chat/start`, data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Send a message in a chat session.
|
|
57
|
+
* @param {string|number} sessionId
|
|
58
|
+
* @param {Object} body - { body, is_internal_note?, attachments? }
|
|
59
|
+
* @returns {Promise<Object>}
|
|
60
|
+
*/
|
|
61
|
+
async function sendMessage(sessionId, body) {
|
|
62
|
+
const message = await apiRequest('POST', `${widgetPath}/chat/${sessionId}/messages`, body);
|
|
63
|
+
return message;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send typing indicator (debounced by caller).
|
|
68
|
+
* @param {string|number} sessionId
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
*/
|
|
71
|
+
let lastTypingSent = 0;
|
|
72
|
+
async function sendTyping(sessionId) {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (now - lastTypingSent < 3000) return;
|
|
75
|
+
lastTypingSent = now;
|
|
76
|
+
try {
|
|
77
|
+
await apiRequest('POST', `${widgetPath}/chat/${sessionId}/typing`);
|
|
78
|
+
} catch {
|
|
79
|
+
// silently ignore typing failures
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* End a chat session.
|
|
85
|
+
* @param {string|number} sessionId
|
|
86
|
+
* @returns {Promise<Object>}
|
|
87
|
+
*/
|
|
88
|
+
async function endChat(sessionId) {
|
|
89
|
+
return apiRequest('POST', `${widgetPath}/chat/${sessionId}/end`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Rate a completed chat session.
|
|
94
|
+
* @param {string|number} sessionId
|
|
95
|
+
* @param {number} rating - 1-5 star rating
|
|
96
|
+
* @param {string} [comment]
|
|
97
|
+
* @returns {Promise<Object>}
|
|
98
|
+
*/
|
|
99
|
+
async function rateChat(sessionId, rating, comment = '') {
|
|
100
|
+
return apiRequest('POST', `${widgetPath}/chat/${sessionId}/rate`, { rating, comment });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check chat availability for a department.
|
|
105
|
+
* @param {string|number} [departmentId]
|
|
106
|
+
* @returns {Promise<Object>} { available, message, estimated_wait }
|
|
107
|
+
*/
|
|
108
|
+
async function checkAvailability(departmentId = null) {
|
|
109
|
+
const query = departmentId ? `?department_id=${departmentId}` : '';
|
|
110
|
+
return apiRequest('GET', `${widgetPath}/chat/availability${query}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Subscribe to real-time chat events for a session.
|
|
115
|
+
* @param {string|number} sessionId
|
|
116
|
+
* @param {Object} callbacks
|
|
117
|
+
* @param {Function} [callbacks.onMessage]
|
|
118
|
+
* @param {Function} [callbacks.onTyping]
|
|
119
|
+
* @param {Function} [callbacks.onEnded]
|
|
120
|
+
* @param {Function} [callbacks.onAssigned]
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
function subscribeToChat(sessionId, callbacks = {}) {
|
|
124
|
+
if (!echoAvailable.value) return () => {};
|
|
125
|
+
|
|
126
|
+
const channelName = `escalated.chat.${sessionId}`;
|
|
127
|
+
const unsubs = [];
|
|
128
|
+
|
|
129
|
+
if (callbacks.onMessage) {
|
|
130
|
+
unsubs.push(listen(channelName, '.chat.message', callbacks.onMessage));
|
|
131
|
+
}
|
|
132
|
+
if (callbacks.onTyping) {
|
|
133
|
+
unsubs.push(listen(channelName, '.chat.typing', callbacks.onTyping));
|
|
134
|
+
}
|
|
135
|
+
if (callbacks.onEnded) {
|
|
136
|
+
unsubs.push(listen(channelName, '.chat.ended', callbacks.onEnded));
|
|
137
|
+
}
|
|
138
|
+
if (callbacks.onAssigned) {
|
|
139
|
+
unsubs.push(listen(channelName, '.chat.assigned', callbacks.onAssigned));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
connectionState.value = 'connected';
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
unsubs.forEach((fn) => fn());
|
|
146
|
+
connectionState.value = 'disconnected';
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Subscribe to the chat queue channel for new incoming chats.
|
|
152
|
+
* @param {Object} callbacks
|
|
153
|
+
* @param {Function} [callbacks.onNewChat]
|
|
154
|
+
* @param {Function} [callbacks.onChatAccepted]
|
|
155
|
+
* @returns {Function} Unsubscribe function
|
|
156
|
+
*/
|
|
157
|
+
function subscribeToChatQueue(callbacks = {}) {
|
|
158
|
+
if (!echoAvailable.value) return () => {};
|
|
159
|
+
|
|
160
|
+
const channelName = 'escalated.chat.queue';
|
|
161
|
+
const unsubs = [];
|
|
162
|
+
|
|
163
|
+
if (callbacks.onNewChat) {
|
|
164
|
+
unsubs.push(listen(channelName, '.chat.new', callbacks.onNewChat));
|
|
165
|
+
}
|
|
166
|
+
if (callbacks.onChatAccepted) {
|
|
167
|
+
unsubs.push(listen(channelName, '.chat.accepted', callbacks.onChatAccepted));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return () => {
|
|
171
|
+
unsubs.forEach((fn) => fn());
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
connectionState: readonly(connectionState),
|
|
177
|
+
messageBuffer: readonly(messageBuffer),
|
|
178
|
+
startChat,
|
|
179
|
+
sendMessage,
|
|
180
|
+
sendTyping,
|
|
181
|
+
endChat,
|
|
182
|
+
rateChat,
|
|
183
|
+
checkAvailability,
|
|
184
|
+
subscribeToChat,
|
|
185
|
+
subscribeToChatQueue,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { ref, computed } from 'vue';
|
|
2
|
-
import locales from '../locales/index.js';
|
|
3
|
-
|
|
4
|
-
const currentLocale = ref('en');
|
|
5
|
-
const customMessages = ref({});
|
|
6
|
-
|
|
7
|
-
function resolve(obj, path) {
|
|
8
|
-
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function applyReplacements(str, replacements) {
|
|
12
|
-
if (!replacements) return str;
|
|
13
|
-
return Object.entries(replacements).reduce(
|
|
14
|
-
(result, [key, value]) => result.replace(new RegExp(`:${key}`, 'g'), value),
|
|
15
|
-
str,
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function setLocale(locale) {
|
|
20
|
-
currentLocale.value = locale;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getLocale() {
|
|
24
|
-
return currentLocale.value;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function mergeMessages(locale, messages) {
|
|
28
|
-
customMessages.value = {
|
|
29
|
-
...customMessages.value,
|
|
30
|
-
[locale]: deepMerge(customMessages.value[locale] || {}, messages),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function deepMerge(target, source) {
|
|
35
|
-
const result = { ...target };
|
|
36
|
-
for (const key of Object.keys(source)) {
|
|
37
|
-
if (
|
|
38
|
-
source[key] &&
|
|
39
|
-
typeof source[key] === 'object' &&
|
|
40
|
-
!Array.isArray(source[key]) &&
|
|
41
|
-
target[key] &&
|
|
42
|
-
typeof target[key] === 'object'
|
|
43
|
-
) {
|
|
44
|
-
result[key] = deepMerge(target[key], source[key]);
|
|
45
|
-
} else {
|
|
46
|
-
result[key] = source[key];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function useI18n() {
|
|
53
|
-
const locale = computed(() => currentLocale.value);
|
|
54
|
-
|
|
55
|
-
function t(key, replacements) {
|
|
56
|
-
const lang = currentLocale.value;
|
|
57
|
-
|
|
58
|
-
// Try custom messages for current locale
|
|
59
|
-
const custom = resolve(customMessages.value[lang], key);
|
|
60
|
-
if (typeof custom === 'string') return applyReplacements(custom, replacements);
|
|
61
|
-
|
|
62
|
-
// Try built-in messages for current locale
|
|
63
|
-
const msg = resolve(locales[lang], key);
|
|
64
|
-
if (typeof msg === 'string') return applyReplacements(msg, replacements);
|
|
65
|
-
|
|
66
|
-
// Fallback to English
|
|
67
|
-
if (lang !== 'en') {
|
|
68
|
-
const customEn = resolve(customMessages.value.en, key);
|
|
69
|
-
if (typeof customEn === 'string') return applyReplacements(customEn, replacements);
|
|
70
|
-
|
|
71
|
-
const fallback = resolve(locales.en, key);
|
|
72
|
-
if (typeof fallback === 'string') return applyReplacements(fallback, replacements);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Return raw key
|
|
76
|
-
return key;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return { t, locale };
|
|
80
|
-
}
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
import locales from '../locales/index.js';
|
|
3
|
+
|
|
4
|
+
const currentLocale = ref('en');
|
|
5
|
+
const customMessages = ref({});
|
|
6
|
+
|
|
7
|
+
function resolve(obj, path) {
|
|
8
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function applyReplacements(str, replacements) {
|
|
12
|
+
if (!replacements) return str;
|
|
13
|
+
return Object.entries(replacements).reduce(
|
|
14
|
+
(result, [key, value]) => result.replace(new RegExp(`:${key}`, 'g'), value),
|
|
15
|
+
str,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setLocale(locale) {
|
|
20
|
+
currentLocale.value = locale;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getLocale() {
|
|
24
|
+
return currentLocale.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function mergeMessages(locale, messages) {
|
|
28
|
+
customMessages.value = {
|
|
29
|
+
...customMessages.value,
|
|
30
|
+
[locale]: deepMerge(customMessages.value[locale] || {}, messages),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deepMerge(target, source) {
|
|
35
|
+
const result = { ...target };
|
|
36
|
+
for (const key of Object.keys(source)) {
|
|
37
|
+
if (
|
|
38
|
+
source[key] &&
|
|
39
|
+
typeof source[key] === 'object' &&
|
|
40
|
+
!Array.isArray(source[key]) &&
|
|
41
|
+
target[key] &&
|
|
42
|
+
typeof target[key] === 'object'
|
|
43
|
+
) {
|
|
44
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
45
|
+
} else {
|
|
46
|
+
result[key] = source[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useI18n() {
|
|
53
|
+
const locale = computed(() => currentLocale.value);
|
|
54
|
+
|
|
55
|
+
function t(key, replacements) {
|
|
56
|
+
const lang = currentLocale.value;
|
|
57
|
+
|
|
58
|
+
// Try custom messages for current locale
|
|
59
|
+
const custom = resolve(customMessages.value[lang], key);
|
|
60
|
+
if (typeof custom === 'string') return applyReplacements(custom, replacements);
|
|
61
|
+
|
|
62
|
+
// Try built-in messages for current locale
|
|
63
|
+
const msg = resolve(locales[lang], key);
|
|
64
|
+
if (typeof msg === 'string') return applyReplacements(msg, replacements);
|
|
65
|
+
|
|
66
|
+
// Fallback to English
|
|
67
|
+
if (lang !== 'en') {
|
|
68
|
+
const customEn = resolve(customMessages.value.en, key);
|
|
69
|
+
if (typeof customEn === 'string') return applyReplacements(customEn, replacements);
|
|
70
|
+
|
|
71
|
+
const fallback = resolve(locales.en, key);
|
|
72
|
+
if (typeof fallback === 'string') return applyReplacements(fallback, replacements);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Return raw key
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { t, locale };
|
|
80
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { onMounted, onUnmounted, unref } from 'vue';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Composable for registering keyboard shortcuts.
|
|
5
|
-
*
|
|
6
|
-
* @param {Object} shortcuts - Map of key to handler function, e.g. { 'r': () => {}, 'n': () => {} }
|
|
7
|
-
* @param {Object} options - Optional configuration
|
|
8
|
-
* @param {import('vue').Ref<boolean>} options.enabled - Ref controlling whether shortcuts are active (default: true)
|
|
9
|
-
*/
|
|
10
|
-
export function useKeyboardShortcuts(shortcuts, options = {}) {
|
|
11
|
-
const ignoredTags = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
|
|
12
|
-
|
|
13
|
-
function handleKeydown(event) {
|
|
14
|
-
// Check if shortcuts are enabled
|
|
15
|
-
if (options.enabled !== undefined && !unref(options.enabled)) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Skip when focus is inside form elements or contenteditable
|
|
20
|
-
const target = event.target;
|
|
21
|
-
if (target && (ignoredTags.has(target.tagName) || target.isContentEditable)) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Skip if modifier keys are held (allow shift for ? and similar)
|
|
26
|
-
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const key = event.key;
|
|
31
|
-
const handler = shortcuts[key];
|
|
32
|
-
|
|
33
|
-
if (handler && typeof handler === 'function') {
|
|
34
|
-
event.preventDefault();
|
|
35
|
-
handler(event);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
onMounted(() => {
|
|
40
|
-
document.addEventListener('keydown', handleKeydown);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
onUnmounted(() => {
|
|
44
|
-
document.removeEventListener('keydown', handleKeydown);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
1
|
+
import { onMounted, onUnmounted, unref } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Composable for registering keyboard shortcuts.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} shortcuts - Map of key to handler function, e.g. { 'r': () => {}, 'n': () => {} }
|
|
7
|
+
* @param {Object} options - Optional configuration
|
|
8
|
+
* @param {import('vue').Ref<boolean>} options.enabled - Ref controlling whether shortcuts are active (default: true)
|
|
9
|
+
*/
|
|
10
|
+
export function useKeyboardShortcuts(shortcuts, options = {}) {
|
|
11
|
+
const ignoredTags = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
|
|
12
|
+
|
|
13
|
+
function handleKeydown(event) {
|
|
14
|
+
// Check if shortcuts are enabled
|
|
15
|
+
if (options.enabled !== undefined && !unref(options.enabled)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Skip when focus is inside form elements or contenteditable
|
|
20
|
+
const target = event.target;
|
|
21
|
+
if (target && (ignoredTags.has(target.tagName) || target.isContentEditable)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Skip if modifier keys are held (allow shift for ? and similar)
|
|
26
|
+
if (event.ctrlKey || event.metaKey || event.altKey) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const key = event.key;
|
|
31
|
+
const handler = shortcuts[key];
|
|
32
|
+
|
|
33
|
+
if (handler && typeof handler === 'function') {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
handler(event);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onMounted(() => {
|
|
40
|
+
document.addEventListener('keydown', handleKeydown);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
onUnmounted(() => {
|
|
44
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
45
|
+
});
|
|
46
|
+
}
|