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 }}

-
+
+