From db1793e149942c442e6c3b0cbe599ac0eeb01f29 Mon Sep 17 00:00:00 2001 From: arys Date: Sun, 30 Aug 2026 12:48:42 +0500 Subject: [PATCH] fix: show AI call transcript in operator voice workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OperatorView never fetched asterisk-bridge's GET /asterisk/live-calls/{call_id}/ai-summary endpoint for voice calls — the useEffect that loads conversation content only covered telegram and whatsapp threads, so the voice panel showed just a Call ID and AI state string, no transcript of what was actually said, even though the backend already had the full data (transcript_segments, customer_request_text, ai_outcome_text). Add BackendVoiceAISummary types, poll the ai-summary endpoint while a voice interaction is selected, and render the transcript + AI request/outcome summary in VoiceWorkspace. --- src/lib/api.ts | 27 ++++++ src/pages/dashboards/OperatorView.tsx | 115 +++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index 568eb0c..af7b72f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -273,6 +273,33 @@ export interface BackendEscalation { completed_at?: string | null; } +export interface BackendVoiceAISummaryTranscriptSegment { + speaker: 'caller' | 'assistant'; + text: string; + sequence_no: number; + source_type: string; + created_at: string; + interrupted?: boolean; +} + +export interface BackendVoiceAISummary { + call_id: string; + session_id?: string | null; + voice_session_id?: string | null; + status_label: string; + status_tone: 'answered' | 'handoff'; + customer_name_status?: string | null; + customer_name_value?: string | null; + customer_name_source?: string | null; + voice_start_language?: string | null; + customer_request_text: string; + ai_outcome_text: string; + handoff_reason: string; + recommended_next_step: string; + generated_at: string; + transcript_segments: BackendVoiceAISummaryTranscriptSegment[]; +} + export class ApiError extends Error { status: number; detail: unknown; diff --git a/src/pages/dashboards/OperatorView.tsx b/src/pages/dashboards/OperatorView.tsx index 921a93e..7b72387 100644 --- a/src/pages/dashboards/OperatorView.tsx +++ b/src/pages/dashboards/OperatorView.tsx @@ -33,6 +33,7 @@ import { BackendLiveCall, BackendThread, BackendThreadMessage, + BackendVoiceAISummary, channelFromBackend, } from '../../lib/api'; @@ -141,6 +142,8 @@ export const OperatorView: React.FC<{ channelFilter?: ChannelType; title?: strin const [items, setItems] = React.useState([]); const [selectedInteraction, setSelectedInteraction] = React.useState(null); const [messages, setMessages] = React.useState([]); + const [voiceSummary, setVoiceSummary] = React.useState(null); + const [voiceSummaryLoading, setVoiceSummaryLoading] = React.useState(false); const [replyText, setReplyText] = React.useState(''); const [isLoading, setIsLoading] = React.useState(true); const [isWorking, setIsWorking] = React.useState(false); @@ -202,6 +205,31 @@ export const OperatorView: React.FC<{ channelFilter?: ChannelType; title?: strin .catch(() => setMessages([])); }, [selectedInteraction]); + const loadVoiceSummary = React.useCallback(async (callId: string) => { + setVoiceSummaryLoading(true); + try { + const summary = await apiRequest( + 'asterisk-bridge', + `asterisk/live-calls/${encodeURIComponent(callId)}/ai-summary`, + ); + setVoiceSummary(summary); + } catch { + setVoiceSummary(null); + } finally { + setVoiceSummaryLoading(false); + } + }, []); + + React.useEffect(() => { + if (!selectedInteraction || selectedInteraction.source !== 'voice') { + setVoiceSummary(null); + return; + } + loadVoiceSummary(selectedInteraction.backendId); + const timer = window.setInterval(() => loadVoiceSummary(selectedInteraction.backendId), 5000); + return () => window.clearInterval(timer); + }, [selectedInteraction, loadVoiceSummary]); + const visibleItems = React.useMemo(() => { const channelItems = channelFilter ? items.filter((item) => item.channel === channelFilter) : items; if (activeTab === 'mine') { @@ -449,7 +477,14 @@ export const OperatorView: React.FC<{ channelFilter?: ChannelType; title?: strin
{selectedInteraction.channel === ChannelType.VOICE ? ( - handleTakeInWork(selectedInteraction)} onHangup={handleFinish} isWorking={isWorking} /> + handleTakeInWork(selectedInteraction)} + onHangup={handleFinish} + isWorking={isWorking} + summary={voiceSummary} + summaryLoading={voiceSummaryLoading} + /> ) : ( <>
@@ -607,12 +642,72 @@ const MessageBubble: React.FC<{ message: BackendThreadMessage }> = ({ message }) ); }; +const voiceSpeakerLabel: Record<'caller' | 'assistant', string> = { + caller: 'Клиент', + assistant: 'ИИ-оператор', +}; + +const VoiceTranscriptPanel: React.FC<{ + summary: BackendVoiceAISummary | null; + loading: boolean; +}> = ({ summary, loading }) => { + if (!summary) { + return ( +
+ {loading ? 'Загрузка транскрипта…' : 'Транскрипт пока недоступен — AI ещё не обработал этот звонок.'} +
+ ); + } + return ( +
+ {(summary.customer_request_text || summary.ai_outcome_text) && ( +
+ {summary.customer_request_text && ( +
+
Запрос клиента
+
{summary.customer_request_text}
+
+ )} + {summary.ai_outcome_text && ( +
+
Ответ ИИ
+
{summary.ai_outcome_text}
+
+ )} +
+ )} +
+ {summary.transcript_segments.map((segment) => ( +
+ + {voiceSpeakerLabel[segment.speaker]} + + + {segment.text} + +
+ ))} +
+
+ ); +}; + const VoiceWorkspace: React.FC<{ interaction: UnifiedInteraction; onClaim: () => void; onHangup: () => void; isWorking: boolean; -}> = ({ interaction, onClaim, onHangup, isWorking }) => { + summary: BackendVoiceAISummary | null; + summaryLoading: boolean; +}> = ({ interaction, onClaim, onHangup, isWorking, summary, summaryLoading }) => { if (interaction.status === 'PENDING') { return (
@@ -665,19 +760,15 @@ const VoiceWorkspace: React.FC<{
-
+
- AI состояние звонка -
-
-

- Call ID: {interaction.backendId} -

-

- AI: {interaction.aiState || 'нет активной AI-сессии'} -

+ Транскрипт разговора с ИИ + + Call ID: {interaction.backendId} · {interaction.aiState || 'нет активной AI-сессии'} +
+
);