From 5fb8113ed72ff1ccbad25fd9c0a07de73a217a45 Mon Sep 17 00:00:00 2001
From: Ruslan Bakiev <572431+veikab@users.noreply.github.com>
Date: Mon, 23 Feb 2026 18:42:21 +0700
Subject: [PATCH] feat(communications): add play/pause for call voice messages
---
.../components/workspace/CrmWorkspaceApp.vue | 116 +++++++++++++++++-
1 file changed, 113 insertions(+), 3 deletions(-)
diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue
index 4300b65..0e47810 100644
--- a/frontend/app/components/workspace/CrmWorkspaceApp.vue
+++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue
@@ -492,6 +492,8 @@ let pilotWaveRecordPlugin: any = null;
let pilotWaveMicSession: { onDestroy: () => void; onEnd: () => void } | null = null;
const commCallWaveHosts = new Map();
const commCallWaveSurfers = new Map();
+const commCallPlayableById = ref>({});
+const commCallPlayingById = ref>({});
const callTranscriptOpen = ref>({});
const callTranscriptLoading = ref>({});
const callTranscriptText = ref>({});
@@ -1233,6 +1235,12 @@ function destroyCommCallWave(itemId: string) {
if (!ws) return;
ws.destroy();
commCallWaveSurfers.delete(itemId);
+ const nextPlayable = { ...commCallPlayableById.value };
+ delete nextPlayable[itemId];
+ commCallPlayableById.value = nextPlayable;
+ const nextPlaying = { ...commCallPlayingById.value };
+ delete nextPlaying[itemId];
+ commCallPlayingById.value = nextPlaying;
}
function destroyAllCommCallWaves() {
@@ -1242,6 +1250,31 @@ function destroyAllCommCallWaves() {
commCallWaveHosts.clear();
}
+function setCommCallPlaying(itemId: string, value: boolean) {
+ commCallPlayingById.value = {
+ ...commCallPlayingById.value,
+ [itemId]: value,
+ };
+}
+
+function isCommCallPlaying(itemId: string) {
+ return Boolean(commCallPlayingById.value[itemId]);
+}
+
+function isCommCallPlayable(item: CommItem) {
+ const known = commCallPlayableById.value[item.id];
+ if (typeof known === "boolean") return known;
+ return Boolean(getCallAudioUrl(item));
+}
+
+function pauseOtherCommCallWaves(currentItemId: string) {
+ for (const [itemId, ws] of commCallWaveSurfers.entries()) {
+ if (itemId === currentItemId) continue;
+ ws.pause?.();
+ setCommCallPlaying(itemId, false);
+ }
+}
+
function parseDurationToSeconds(raw?: string) {
if (!raw) return 0;
const text = raw.trim().toLowerCase();
@@ -1315,6 +1348,7 @@ async function ensureCommCallWave(itemId: string) {
parseDurationToSeconds(callItem.duration) ||
Math.max(8, Math.min(120, Math.round(((callItem.transcript ?? []).join(" ").length || callItem.text.length) / 10)));
const peaks = buildCallWavePeaks(callItem, 360);
+ const audioUrl = getCallAudioUrl(callItem);
const ws = WaveSurfer.create({
container: host,
@@ -1322,12 +1356,32 @@ async function ensureCommCallWave(itemId: string) {
waveColor: "rgba(180, 206, 255, 0.88)",
progressColor: "rgba(118, 157, 248, 0.95)",
cursorWidth: 0,
- interact: false,
+ interact: Boolean(audioUrl),
normalize: true,
barWidth: 0,
});
- await ws.load("", [peaks], durationSeconds);
+ ws.on("play", () => setCommCallPlaying(itemId, true));
+ ws.on("pause", () => setCommCallPlaying(itemId, false));
+ ws.on("finish", () => setCommCallPlaying(itemId, false));
+
+ let playable = false;
+ if (audioUrl) {
+ try {
+ await ws.load(audioUrl, [peaks], durationSeconds);
+ playable = true;
+ } catch {
+ await ws.load("", [peaks], durationSeconds);
+ playable = false;
+ }
+ } else {
+ await ws.load("", [peaks], durationSeconds);
+ }
+
+ commCallPlayableById.value = {
+ ...commCallPlayableById.value,
+ [itemId]: playable,
+ };
commCallWaveSurfers.set(itemId, ws);
}
@@ -4213,6 +4267,20 @@ function isCallTranscriptOpen(itemId: string) {
return Boolean(callTranscriptOpen.value[itemId]);
}
+async function toggleCommCallPlayback(item: CommItem) {
+ if (!isCommCallPlayable(item)) return;
+ const itemId = item.id;
+ await ensureCommCallWave(itemId);
+ const ws = commCallWaveSurfers.get(itemId);
+ if (!ws) return;
+ if (isCommCallPlaying(itemId)) {
+ ws.pause?.();
+ return;
+ }
+ pauseOtherCommCallWaves(itemId);
+ await ws.play?.();
+}
+
function channelIcon(channel: "All" | CommItem["channel"]) {
if (channel === "All") return "all";
if (channel === "Telegram") return "telegram";
@@ -5039,7 +5107,27 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
ยท {{ entry.item.duration }}
-
+
+